diff --git a/.gitignore b/.gitignore index 98d70943c..e22f1bb93 100644 --- a/.gitignore +++ b/.gitignore @@ -1,61 +1,23 @@ ####################################### BDArmory Ignore ####################################### -BDArmory/BDArmory.csproj.user +*.user +*.userprefs _LocalDev +LocalDev -BahaTurret/.vs -BahaTurret/obj -BahaTurret/bin/Debug/ -BDArmory/.vs -BDArmory/obj -BDArmory/bin/Debug/ - -BahaTurret/.vs/config -BahaTurret/LocalDev/7za_dir.txt -BahaTurret/LocalDev/dist_dir.txt -BahaTurret/LocalDev/ksp_dir.txt -BahaTurret/LocalDev/ksp_production_dir.txt -BahaTurret/LocalDev/mono_exe.txt -BahaTurret/LocalDev/pdb2mdb_exe.txt -BahaTurret/BahaTurret.userprefs -BahaTurret/bin/Release - -BDArmory/.vs -BDArmory.Core/.vs - -BDArmory/obj -BDArmory/bin/Debug/ - -BDArmory/.vs/config -BDArmory/LocalDev/7za_dir.txt -BDArmory/LocalDev/dist_dir.txt -BDArmory/LocalDev/ksp_dir.txt -BDArmory/LocalDev/ksp_production_dir.txt -BDArmory/LocalDev/mono_exe.txt -BDArmory/LocalDev/pdb2mdb_exe.txt -BDArmory/BahaTurret.userprefs -BDArmory/Distribution/GameData/BDArmory/Plugins/BDArmory.Core.dll -BDArmory/Distribution/GameData/BDArmory/Plugins/BDArmory.dll - -BDArmory/packages - -BDArmory.Core/obj/Debug/BDArmory.Core.csproj.FileListAbsolute.txt -BDArmory.Core/obj/Release/BDArmory.Core.csproj.FileListAbsolute.txt -BDArmory/bin/Debug -BDArmory.Core/obj/ -BDArmory.Core/.vs -BDArmory.Core/packages.config -BDArmory/packages.config -BDArmory.Multiplayer/obj -BDArmory.Core/bin/Debug/Assembly-CSharp.dll -BDArmory.Core/bin/Debug/BDArmory.Core.dll -BDArmory.Core/bin/Debug/KSPAssets.dll -BDArmory.Core/bin/Debug/UnityEngine.dll -BahaTurret/BDArmory.sln.DotSettings.user -BahaTurret/BDArmory.csproj.user -BDArmory.Core/bin/Debug/UnityEngine.UI.dll +# Generated or local folders and files +.envrc +.env +.vscode +**/.vs +**/obj +**/bin +**/*.[Cc]ache* +**/packages* +**/UpgradeLog* +BDArmory/Distribution/GameData/BDArmory/Plugins/ ####################################### Unity Ignore @@ -114,52 +76,3 @@ Icon # Files that might appear on external disk .Spotlight-V100 .Trashes -/.vs -/.vs -/.vs/slnx.sqlite -/.vs/ProjectSettings.json -/.vs/BDArmory_JRODRIGV/v15/Browse.VC.db -/.vs/BDArmory_JRODRIGV/v15 -/.vs/slnx.sqlite -/BahaTurret/bin/Release -/BDArmory.Core/obj/Debug/BDArmory.Core.csproj.FileListAbsolute.txt -/BDArmory.Core/obj/Release/BDArmory.Core.csproj.FileListAbsolute.txt -/BDArmory/bin/Debug -BDArmory.Core/obj/ -/BDArmory.Multiplayer/obj -BDArmory.Core/bin/Debug/Assembly-CSharp.dll -BDArmory.Core/bin/Debug/BDArmory.Core.dll -BDArmory.Core/bin/Debug/KSPAssets.dll -BDArmory.Core/bin/Debug/UnityEngine.dll -BahaTurret/BDArmory.sln.DotSettings.user -BahaTurret/BDArmory.csproj.user -BDArmory.Core/bin/Debug/UnityEngine.UI.dll -BDArmory/LocalDev/Refs/UnityEngine.UI.dll -BDArmory/LocalDev/Refs/KSPAssets.dll -BDArmory/LocalDev/Refs/Assembly-CSharp.dll -BDArmory/LocalDev/Refs/Assembly-CSharp-firstpass.dll -BDArmory/LocalDev/Refs/UnityEngine.dll -/BDArmory/UpgradeLog3.htm -/BDArmory/UpgradeLog2.htm -/BDArmory/UpgradeLog.htm -/BDArmory/bin/Release -/BDArmory.Core/bin -/BDArmory.Guidance/bin/Release -/BDArmory.Guidance/obj/Release -/BDArmory/BDArmory.sln.DotSettings.user -/BDArmory.Events/obj/Debug -/BDArmory/LocalDev/Refs -/BDArmory.Events/obj/Release -/Binaries -/BDArmory/Distribution/GameData/BDArmory/Plugins -/BDArmory/_ReSharper.Caches/ReSharperPlatformVs15182_74c703de.BDArmory.00 -/packages/Microsoft.Net.Compilers.2.8.0 -/_ReSharper.Caches/ReSharperPlatformVs15182_74c703de.BDArmory.00 -/BDArmory/_ReSharper.Caches/ReSharperPlatformVs15183_74c703de.BDArmory.00 -/BDArmory/_ReSharper.Caches/ReSharperPlatformVs16191_4011600b.BDArmory.00 -/BDArmory/_ReSharper.Caches/ReSharperPlatformVs16191_4011600b.BDArmory.01 -/_ReSharper.Caches/ReSharperPlatformVs16191_4011600b.00 -/BDArmory/_ReSharper.Caches/ReSharperPlatformVs16193_4011600b.BDArmory.00 -/_ReSharper.Caches/ReSharperPlatformVs16193_4011600b.00 -.envrc -.vscode diff --git a/.gitignore.orig b/.gitignore.orig index 8b3d56564..469976b86 100644 --- a/.gitignore.orig +++ b/.gitignore.orig @@ -98,14 +98,7 @@ Icon /.vs/BDArmory_JRODRIGV/v15 /.vs/slnx.sqlite /BahaTurret/bin/Release -/BDArmory.Core/obj/Debug/BDArmory.Core.csproj.FileListAbsolute.txt -/BDArmory/bin/Debug -/BDArmory.Core/obj +/BDArmory/bin /BDArmory.Multiplayer/obj -BDArmory.Core/bin/Debug/Assembly-CSharp.dll -BDArmory.Core/bin/Debug/BDArmory.Core.dll -BDArmory.Core/bin/Debug/KSPAssets.dll -BDArmory.Core/bin/Debug/UnityEngine.dll BahaTurret/BDArmory.sln.DotSettings.user BahaTurret/BDArmory.csproj.user -BDArmory.Core/bin/Debug/UnityEngine.UI.dll diff --git a/BDArmory.Core/BDAPersistentSettingsField.cs b/BDArmory.Core/BDAPersistentSettingsField.cs deleted file mode 100644 index 726c2a54e..000000000 --- a/BDArmory.Core/BDAPersistentSettingsField.cs +++ /dev/null @@ -1,157 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Reflection; -using UniLinq; -using UnityEngine; - -namespace BDArmory.Core -{ - [AttributeUsage(AttributeTargets.Field)] - public class BDAPersistentSettingsField : Attribute - { - public BDAPersistentSettingsField() - { - } - - public static void Save() - { - ConfigNode fileNode = ConfigNode.Load(BDArmorySettings.settingsConfigURL); - - if (!fileNode.HasNode("BDASettings")) - { - fileNode.AddNode("BDASettings"); - } - - ConfigNode settings = fileNode.GetNode("BDASettings"); - IEnumerator field = typeof(BDArmorySettings).GetFields().AsEnumerable().GetEnumerator(); - while (field.MoveNext()) - { - if (field.Current == null) continue; - if (!field.Current.IsDefined(typeof(BDAPersistentSettingsField), false)) continue; - - var fieldValue = field.Current.GetValue(null); - if (fieldValue.GetType() == typeof(Vector3d)) - { - settings.SetValue(field.Current.Name, ((Vector3d)fieldValue).ToString("G"), true); - } - else if (fieldValue.GetType() == typeof(List)) - { - settings.SetValue(field.Current.Name, string.Join("; ", (List)fieldValue), true); - } - else - { - settings.SetValue(field.Current.Name, fieldValue.ToString(), true); - } - } - field.Dispose(); - fileNode.Save(BDArmorySettings.settingsConfigURL); - } - - public static void Load() - { - ConfigNode fileNode = ConfigNode.Load(BDArmorySettings.settingsConfigURL); - if (!fileNode.HasNode("BDASettings")) return; - - ConfigNode settings = fileNode.GetNode("BDASettings"); - - IEnumerator field = typeof(BDArmorySettings).GetFields().AsEnumerable().GetEnumerator(); - while (field.MoveNext()) - { - if (field.Current == null) continue; - if (!field.Current.IsDefined(typeof(BDAPersistentSettingsField), false)) continue; - - if (!settings.HasValue(field.Current.Name)) continue; - object parsedValue = ParseValue(field.Current.FieldType, settings.GetValue(field.Current.Name)); - if (parsedValue != null) - { - field.Current.SetValue(null, parsedValue); - } - } - field.Dispose(); - } - - public static object ParseValue(Type type, string value) - { - try - { - if (type == typeof(string)) - { - return value; - } - - if (type == typeof(bool)) - { - return Boolean.Parse(value); - } - else if (type.IsEnum) - { - return System.Enum.Parse(type, value); - } - else if (type == typeof(float)) - { - return Single.Parse(value); - } - else if (type == typeof(int)) - { - return int.Parse(value); - } - else if (type == typeof(Single)) - { - return Single.Parse(value); - } - else if (type == typeof(Rect)) - { - string[] strings = value.Split(','); - int xVal = Int32.Parse(strings[0].Split(':')[1].Split('.')[0]); - int yVal = Int32.Parse(strings[1].Split(':')[1].Split('.')[0]); - int wVal = Int32.Parse(strings[2].Split(':')[1].Split('.')[0]); - int hVal = Int32.Parse(strings[3].Split(':')[1].Split('.')[0]); - Rect rectVal = new Rect - { - x = xVal, - y = yVal, - width = wVal, - height = hVal - }; - return rectVal; - } - else if (type == typeof(Vector2d)) - { - char[] charsToTrim = { '(', ')', ' ' }; - string[] strings = value.Trim(charsToTrim).Split(','); - double x = double.Parse(strings[0]); - double y = double.Parse(strings[1]); - return new Vector2d(x, y); - } - else if (type == typeof(Vector3d)) - { - char[] charsToTrim = { '[', ']', ' ' }; - string[] strings = value.Trim(charsToTrim).Split(','); - double x = double.Parse(strings[0]); - double y = double.Parse(strings[1]); - double z = double.Parse(strings[2]); - return new Vector3d(x, y, z); - } - else if (type == typeof(Vector2Int)) - { - char[] charsToTrim = { '(', ')', ' ' }; - string[] strings = value.Trim(charsToTrim).Split(','); - int x = int.Parse(strings[0]); - int y = int.Parse(strings[1]); - return new Vector2Int(x, y); - } - else if (type == typeof(List)) - { - return value.Split(new string[] { "; " }, StringSplitOptions.RemoveEmptyEntries).ToList(); - } - } - catch (Exception e) - { - Debug.LogError("[BDArmory.BDAPersistantSettingsField]: Failed to parse '" + value + "' as a " + type.ToString() + ": " + e.Message); - return null; - } - Debug.LogError("[BDArmory.BDAPersistantSettingsField]: BDAPersistantSettingsField to parse settings field of type " + type + " and value " + value); - return null; - } - } -} diff --git a/BDArmory.Core/BDArmory.Core.csproj b/BDArmory.Core/BDArmory.Core.csproj deleted file mode 100644 index b4d4b4bd0..000000000 --- a/BDArmory.Core/BDArmory.Core.csproj +++ /dev/null @@ -1,124 +0,0 @@ - - - - - Debug - AnyCPU - {A6F1753E-9570-4C40-AF72-A179890582E5} - Library - Properties - BDArmory.Core - BDArmory.Core - v4.8 - 512 - - - - true - portable - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - default - false - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - default - false - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - False - ..\..\_LocalDev\KSPRefs\Assembly-CSharp.dll - - - False - ..\..\_LocalDev\KSPRefs\KSPAssets.dll - - - - False - ..\..\_LocalDev\KSPRefs\UnityEngine.dll - - - ..\..\_LocalDev\KSPRefs\UnityEngine.AnimationModule.dll - - - ..\..\_LocalDev\KSPRefs\UnityEngine.AssetBundleModule.dll - - - ..\..\_LocalDev\KSPRefs\UnityEngine.CoreModule.dll - - - ..\..\_LocalDev\KSPRefs\UnityEngine.ImageConversionModule.dll - - - ..\..\_LocalDev\KSPRefs\UnityEngine.IMGUIModule.dll - - - ..\..\_LocalDev\KSPRefs\UnityEngine.InputLegacyModule.dll - - - ..\..\_LocalDev\KSPRefs\UnityEngine.InputModule.dll - - - ..\..\_LocalDev\KSPRefs\UnityEngine.PhysicsModule.dll - - - ..\..\_LocalDev\KSPRefs\UnityEngine.TextCoreModule.dll - - - ..\..\_LocalDev\KSPRefs\UnityEngine.TextRenderingModule.dll - - - False - ..\..\_LocalDev\KSPRefs\UnityEngine.UI.dll - - - ..\..\_LocalDev\KSPRefs\UnityEngine.UIElementsModule.dll - - - ..\..\_LocalDev\KSPRefs\UnityEngine.UIModule.dll - - - ..\..\_LocalDev\KSPRefs\UnityEngine.UnityWebRequestWWWModule.dll - - - - \ No newline at end of file diff --git a/BDArmory.Core/BuildingDamage.cs b/BDArmory.Core/BuildingDamage.cs deleted file mode 100644 index f4be9f513..000000000 --- a/BDArmory.Core/BuildingDamage.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Collections.Generic; -using UnityEngine; - -namespace BDArmory.Core -{ - [KSPAddon(KSPAddon.Startup.MainMenu, false)] - public class BuildingDamage : ScenarioDestructibles - { - public override void OnAwake() - { - Debug.Log("[BDArmory.BuildingDamage]: Modifying Buildings"); - - foreach (KeyValuePair bldg in protoDestructibles) - { - using (var building = bldg.Value.dBuildingRefs.GetEnumerator()) - while( building.MoveNext()) - { - building.Current.damageDecay = 600f; - building.Current.impactMomentumThreshold *= 150; - } - } - } - } -} diff --git a/BDArmory.Core/Enum/DamageOperation.cs b/BDArmory.Core/Enum/DamageOperation.cs deleted file mode 100644 index 2809cce98..000000000 --- a/BDArmory.Core/Enum/DamageOperation.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace BDArmory.Core.Enum -{ - public enum DamageOperation - { - Set = 0, - Add = 1 - } -} diff --git a/BDArmory.Core/Events/DamageEventArgs.cs b/BDArmory.Core/Events/DamageEventArgs.cs deleted file mode 100644 index 7941b4770..000000000 --- a/BDArmory.Core/Events/DamageEventArgs.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using BDArmory.Core.Enum; - -namespace BDArmory.Core.Events -{ - [Serializable] - public class DamageEventArgs : EventArgs - { - public int VesselId { get; set; } - public int PartId { get; set; } - public float Damage { get; set; } - public float Armor { get; set; } - public DamageOperation Operation { get; set; } - } -} diff --git a/BDArmory.Core/Extension/DamageFX.cs b/BDArmory.Core/Extension/DamageFX.cs deleted file mode 100644 index ea7112864..000000000 --- a/BDArmory.Core/Extension/DamageFX.cs +++ /dev/null @@ -1,44 +0,0 @@ -using UnityEngine; - -namespace BDArmory.Core.Extension -{ - public class DamageFX : MonoBehaviour - { - public static bool engineDamaged = false; - - public void Start() - { - } - - public void FixedUpdate() - { - if (engineDamaged) - { - float probability = Utils.BDAMath.RangedProbability(new[] { 50f, 25f, 20f, 2f }); - if (probability >= 3) - { - ModuleEngines engine = gameObject.GetComponent(); - engine.flameout = true; - engine.heatProduction *= 1.05f; - engine.maxThrust *= 0.825f; - } - } - } - - public static void SetEngineDamage(Part part) - { - ModuleEngines engine; - engine = part.GetComponent(); - engine.flameout = true; - engine.heatProduction *= 1.0125f; - engine.maxThrust *= 0.825f; - } - - public static void SetWingDamage(Part part) - { - ModuleLiftingSurface wing; - wing = part.GetComponent(); - wing.deflectionLiftCoeff *= 0.825f; - } - } -} diff --git a/BDArmory.Core/Extension/VesselExtensions.cs b/BDArmory.Core/Extension/VesselExtensions.cs deleted file mode 100644 index 6fa2a34d6..000000000 --- a/BDArmory.Core/Extension/VesselExtensions.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System; -using System.Collections.Generic; -using UnityEngine; - -namespace BDArmory.Core.Extension -{ - public static class VesselExtensions - { - public static HashSet InOrbitSituations = new HashSet { Vessel.Situations.ORBITING, Vessel.Situations.SUB_ORBITAL, Vessel.Situations.ESCAPING }; - - public static bool InOrbit(this Vessel v) - { - if (v == null) return false; - return InOrbitSituations.Contains(v.situation); - } - - public static bool InVacuum(this Vessel v) - { - return v.atmDensity <= 0.001f; - } - - public static bool IsUnderwater(this Vessel v) - { - if (!v) return false; - return v.altitude < -20; //some boats sit slightly underwater, this is only for submersibles - } - - public static Vector3d Velocity(this Vessel v) - { - try - { - if (v == null) return Vector3d.zero; - if (!v.InOrbit()) - { - return v.srf_velocity; - } - else - { - return v.obt_velocity; - } - } - catch (Exception e) - { - Debug.LogWarning("[BDArmory.VesselExtensions]: Exception thrown in Velocity: " + e.Message + "\n" + e.StackTrace); - //return v.srf_velocity; - return new Vector3d(0, 0, 0); - } - } - - public static double GetFutureAltitude(this Vessel vessel, float predictionTime = 10) - { - Vector3 futurePosition = vessel.CoM + vessel.Velocity() * predictionTime - + 0.5f * vessel.acceleration_immediate * Mathf.Pow(predictionTime, 2); - - return GetRadarAltitudeAtPos(futurePosition); - } - - public static Vector3 GetFuturePosition(this Vessel vessel, float predictionTime = 10) - { - return vessel.CoM + vessel.Velocity() * predictionTime + 0.5f * vessel.acceleration_immediate * Math.Pow(predictionTime, 2); - } - - public static float GetRadarAltitudeAtPos(Vector3 position) - { - double latitudeAtPos = FlightGlobals.currentMainBody.GetLatitude(position); - double longitudeAtPos = FlightGlobals.currentMainBody.GetLongitude(position); - - float radarAlt = Mathf.Clamp( - (float)(FlightGlobals.currentMainBody.GetAltitude(position) - - FlightGlobals.currentMainBody.TerrainAltitude(latitudeAtPos, longitudeAtPos)), 0, - (float)FlightGlobals.currentMainBody.GetAltitude(position)); - return radarAlt; - } - - // Get a vessel's "radius". - public static float GetRadius(this Vessel vessel, bool average = false) - { - // Get vessel size. - Vector3 size = vessel.vesselSize; - - if (average) // Get the average of the dimensions. - return (size.x + size.y + size.z) / 6f; - - // Get largest dimension. - return Mathf.Max(Mathf.Max(size.x, size.y), size.z) / 2f; - } - } -} diff --git a/BDArmory.Core/Module/HitpointTracker.cs b/BDArmory.Core/Module/HitpointTracker.cs deleted file mode 100644 index 512c3e4d6..000000000 --- a/BDArmory.Core/Module/HitpointTracker.cs +++ /dev/null @@ -1,1177 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using BDArmory.Core.Extension; -using BDArmory.Core.Utils; -using KSP.Localization; -using UnityEngine; - -namespace BDArmory.Core.Module -{ - public class HitpointTracker : PartModule, IPartMassModifier, IPartCostModifier - { - #region KSP Fields - public float GetModuleMass(float baseMass, ModifierStagingSituation situation) => armorMass + HullMassAdjust; - - public ModifierChangeWhen GetModuleMassChangeWhen() => ModifierChangeWhen.FIXED; - public float GetModuleCost(float baseCost, ModifierStagingSituation situation) => armorCost + HullCostAdjust; - public ModifierChangeWhen GetModuleCostChangeWhen() => ModifierChangeWhen.FIXED; - - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_Hitpoints"),//Hitpoints - UI_ProgressBar(affectSymCounterparts = UI_Scene.None, controlEnabled = false, scene = UI_Scene.All, maxValue = 100000, minValue = 0, requireFullControl = false)] - public float Hitpoints; - - [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_ArmorThickness"),//Armor Thickness - UI_FloatRange(minValue = 0f, maxValue = 200, stepIncrement = 1f, scene = UI_Scene.All)] - public float Armor = 10f; //settable Armor thickness availible for editing in the SPH?VAB - - [KSPField(advancedTweakable = true, guiActive = true, guiActiveEditor = false, guiName = "#LOC_BDArmory_ArmorThickness")]//armor Thickness - public float Armour = 10f; - - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = false, guiName = "#LOC_BDArmory_ArmorRemaining"),//Armor intregity - UI_ProgressBar(affectSymCounterparts = UI_Scene.None, controlEnabled = false, scene = UI_Scene.Flight, maxValue = 100, minValue = 0, requireFullControl = false)] - public float ArmorRemaining; - - public float StartingArmor; - - [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_Armor_ArmorType"),//Armor Types - UI_FloatRange(minValue = 1, maxValue = 999, stepIncrement = 1, scene = UI_Scene.All)] - public float ArmorTypeNum = 1; //replace with prev/next buttons? //or a popup GUI box with a list of selectable types... - - //Add a part material type setting, so parts can be selected to be made out of wood/aluminium/steel to adjust base partmass/HP? - [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_Armor_HullType"),//hull material Types - UI_FloatRange(minValue = 1, maxValue = 3, stepIncrement = 1, scene = UI_Scene.Editor)] - public float HullTypeNum = 2; - private float OldHullType = -1; - - [KSPField(guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_Armor_HullMat")]//Status - public string guiHullTypeString = Localizer.Format("#LOC_BDArmory_Aluminium"); - - public float HullMassAdjust = 0f; - public float HullCostAdjust = 0f; - double resourceCost = 0; - - private bool IgnoreForArmorSetup = false; - - private bool isAI = false; - - private bool isProcWing = false; - private bool waitingForHullSetup = false; - private float OldArmorType = -1; - - [KSPField(advancedTweakable = true, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_ArmorMass")]//armor mass - public float armorMass = 0f; - - [KSPField(advancedTweakable = true, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_ArmorCost")]//armor cost - public float armorCost = 0f; - - [KSPField(isPersistant = true)] - public string SelectedArmorType = "None"; //presumably Aubranium can use this to filter allowed/banned types - - [KSPField(guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_ArmorCurrent")]//Status - public string guiArmorTypeString = "def"; - - private ArmorInfo armorInfo; - - private bool armorReset = false; - - [KSPField(isPersistant = true)] - public float maxHitPoints = -1f; - - [KSPField(isPersistant = true)] - public float ArmorThickness = 0f; - - [KSPField(isPersistant = true)] - public bool ArmorSet; - - [KSPField(isPersistant = true)] - public string ExplodeMode = "Never"; - - [KSPField(isPersistant = true)] - public bool FireFX = true; - - [KSPField(isPersistant = true)] - public float FireFXLifeTimeInSeconds = 5f; - - //Armor Vars - [KSPField(isPersistant = true)] - public float Density; - [KSPField(isPersistant = true)] - public float Diffusivity; - [KSPField(isPersistant = true)] - public float Ductility; - [KSPField(isPersistant = true)] - public float Hardness; - [KSPField(isPersistant = true)] - public float Strength; - [KSPField(isPersistant = true)] - public float SafeUseTemp; - [KSPField(isPersistant = true)] - public float Cost; - - private bool startsArmored = false; - public bool ArmorPanel = false; - //Part vars - private float partMass = 0f; - public Vector3 partSize; - [KSPField(isPersistant = true)] - public float maxSupportedArmor = -1; //upper cap on armor per part, overridable in MM/.cfg - [KSPField(isPersistant = true)] - public float armorVolume = -1; - private float sizeAdjust; - AttachNode bottom; - AttachNode top; - - public List defaultShader; - public List defaultColor; - public bool RegisterProcWingShader = false; - - public float defenseMutator = 1; - - #endregion KSP Fields - - #region Heart Bleed - private double nextHeartBleedTime = 0; - #endregion Heart Bleed - - private readonly float hitpointMultiplier = BDArmorySettings.HITPOINT_MULTIPLIER; - - private float previousHitpoints = -1; - private bool _updateHitpoints = false; - private bool _forceUpdateHitpointsUI = false; - private const int HpRounding = 100; - private bool _updateMass = false; - private bool _armorModified = false; - private bool _hullModified = false; - private bool _armorConfigured = false; - private bool _hullConfigured = false; - private bool _hpConfigured = false; - private bool _finished_setting_up = false; - - public bool isOnFire = false; - - public static bool GameIsPaused - { - get { return PauseMenu.isOpen || Time.timeScale == 0; } - } - - public override void OnLoad(ConfigNode node) - { - base.OnLoad(node); - - if (!HighLogic.LoadedSceneIsEditor && !HighLogic.LoadedSceneIsFlight) return; - - if (part.partInfo == null) - { - // Loading of the prefab from the part config - _updateHitpoints = true; - } - else - { - // Loading of the part from a saved craft - if (HighLogic.LoadedSceneIsEditor) - { - _updateHitpoints = true; - ArmorSet = false; - } - else // Loading of the part from a craft in flight mode - { - if (BDArmorySettings.RESET_HP && part.vessel != null) // Reset Max HP - { - var maxHPString = ConfigNodeUtils.FindPartModuleConfigNodeValue(part.partInfo.partConfig, "HitpointTracker", "maxHitPoints"); - if (!string.IsNullOrEmpty(maxHPString)) // Use the default value from the MM patch. - { - try - { - maxHitPoints = float.Parse(maxHPString); - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.HitpointTracker]: setting maxHitPoints of " + part + " on " + part.vessel.vesselName + " to " + maxHitPoints); - _updateHitpoints = true; - } - catch (Exception e) - { - Debug.LogError("[BDArmory.HitpointTracker]: Failed to parse maxHitPoints configNode: " + e.Message); - } - } - else // Use the stock default value. - { - maxHitPoints = -1f; - } - } - else // Don't. - { - // enabled = false; // We'll disable this later once things are set up. - } - } - } - } - - public void SetupPrefab() - { - if (part != null) - { - var maxHitPoints_ = CalculateTotalHitpoints(); - - if (!_forceUpdateHitpointsUI && previousHitpoints == maxHitPoints_) return; - - //Add Hitpoints - if (!ArmorPanel) - { - UI_ProgressBar damageFieldFlight = (UI_ProgressBar)Fields["Hitpoints"].uiControlFlight; - damageFieldFlight.maxValue = maxHitPoints_; - damageFieldFlight.minValue = 0f; - UI_ProgressBar damageFieldEditor = (UI_ProgressBar)Fields["Hitpoints"].uiControlEditor; - damageFieldEditor.maxValue = maxHitPoints_; - damageFieldEditor.minValue = 0f; - } - else - { - Fields["Hitpoints"].guiActive = false; - Fields["Hitpoints"].guiActiveEditor = false; - } - Hitpoints = maxHitPoints_; - ArmorRemaining = 100; - if (!ArmorSet) overrideArmorSetFromConfig(); - - previousHitpoints = maxHitPoints_; - part.RefreshAssociatedWindows(); - } - else - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.HitpointTracker]: OnStart part is null"); - } - } - - public override void OnStart(StartState state) - { - if (part == null) return; - isEnabled = true; - if (part.name.Contains("B9.Aero.Wing.Procedural")) - { - isProcWing = true; - } - StartingArmor = Armor; - if (part.name.ToLower().Contains("armor")) - { - ArmorPanel = true; - } - if (HighLogic.LoadedSceneIsFlight) - { - if (BDArmorySettings.RESET_ARMOUR) - { - ArmorSetup(null, null); - } - if (BDArmorySettings.RESET_HULL || ArmorPanel) - { - IgnoreForArmorSetup = true; - HullTypeNum = 2; - SetHullMass(); - } - - part.RefreshAssociatedWindows(); - } - if (HighLogic.LoadedSceneIsFlight || HighLogic.LoadedSceneIsEditor) - { - int typecount = 0; - for (int i = 0; i < ArmorInfo.armorNames.Count; i++) - { - typecount++; - } - if (part.name == "bdPilotAI" || part.name == "bdShipAI" || part.name == "missileController" || part.name == "bdammGuidanceModule") - { - isAI = true; - Fields["ArmorTypeNum"].guiActiveEditor = false; - Fields["guiArmorTypeString"].guiActiveEditor = false; - Fields["guiArmorTypeString"].guiActive = false; - Fields["guiHullTypeString"].guiActiveEditor = false; - Fields["guiHullTypeString"].guiActive = false; - Fields["armorCost"].guiActiveEditor = false; - Fields["armorMass"].guiActiveEditor = false; - //UI_ProgressBar Armorleft = (UI_ProgressBar)Fields["ArmorRemaining"].uiControlFlight; - //Armorleft.scene = UI_Scene.None; - } - if (part.IsMissile()) - { - Fields["ArmorTypeNum"].guiActiveEditor = false; - Fields["guiArmorTypeString"].guiActiveEditor = false; - Fields["armorCost"].guiActiveEditor = false; - Fields["armorMass"].guiActiveEditor = false; - } - UI_FloatRange ATrangeEditor = (UI_FloatRange)Fields["ArmorTypeNum"].uiControlEditor; - ATrangeEditor.onFieldChanged = ArmorModified; - ATrangeEditor.maxValue = (float)typecount; - if (isAI || part.IsMissile()) - { - Fields["ArmorTypeNum"].guiActiveEditor = false; - ATrangeEditor.maxValue = 1; - } - if (BDArmorySettings.LEGACY_ARMOR || BDArmorySettings.RESET_ARMOUR) - { - Fields["ArmorTypeNum"].guiActiveEditor = false; - Fields["guiArmorTypeString"].guiActiveEditor = false; - Fields["guiArmorTypeString"].guiActive = false; - Fields["armorCost"].guiActiveEditor = false; - Fields["armorMass"].guiActiveEditor = false; - ATrangeEditor.maxValue = 1; - } - UI_FloatRange HTrangeEditor = (UI_FloatRange)Fields["HullTypeNum"].uiControlEditor; - HTrangeEditor.onFieldChanged = HullModified; - //if part is an engine/fueltank don't allow wood construction/mass reduction - if (part.IsMissile() || part.IsWeapon() || ArmorPanel || isAI || BDArmorySettings.LEGACY_ARMOR || BDArmorySettings.RESET_HULL) - { - HullTypeNum = 2; - HTrangeEditor.minValue = 2; - HTrangeEditor.maxValue = 2; - Fields["HullTypeNum"].guiActiveEditor = false; - Fields["HullTypeNum"].guiActive = false; - Fields["guiHullTypeString"].guiActiveEditor = false; - Fields["guiHullTypeString"].guiActive = false; - IgnoreForArmorSetup = true; - SetHullMass(); - } - if (ArmorThickness > 10 || ArmorPanel) //Set to 10, Cerulean's HP MM patches all have armorThickness 10 fields - { - startsArmored = true; - if (Armor > 10 && Armor != ArmorThickness) - { } - else - { - Armor = ArmorThickness; - } - //if (ArmorTypeNum == 1) - //{ - // ArmorTypeNum = 2; - //} - } - } - GameEvents.onEditorShipModified.Add(ShipModified); - GameEvents.onPartDie.Add(OnPartDie); - bottom = part.FindAttachNode("bottom"); - top = part.FindAttachNode("top"); - int topSize = 0; - int bottomSize = 0; - try - { - if (top != null) - { - topSize = top.size; - } - if (bottom != null) - { - bottomSize = bottom.size; - } - } - catch - { - Debug.Log("[BDArmoryCore.HitpointTracker]: no node size detected"); - } - //if attachnode top != bottom, then cone. is nodesize Attachnode.radius or Attachnode.size? - //getSize returns size of a rectangular prism; most parts are circular, some are conical; use sizeAdjust to compensate - if (bottom != null && top != null) //cylinder - { - sizeAdjust = 0.783f; - } - else if ((bottom == null && top != null) || (bottom != null && top == null) || (topSize > bottomSize || bottomSize > topSize)) //cone - { - sizeAdjust = 0.422f; - } - else //no bottom or top nodes, assume srf attached part; these are usually panels of some sort. Will need to determine method of ID'ing triangular panels/wings - { //Wings at least could use WingLiftArea as a workaround for approx. surface area... - sizeAdjust = 0.5f; //armor on one side, otherwise will have armor thickness on both sides of the panel, nonsensical + doiuble weight - } - partSize = CalcPartBounds(this.part, this.transform).size; - if (armorVolume < 0) //make this persistant to get around diffeences in part bounds between SPH/Flight. - { - armorVolume = // thickness * armor mass; moving it to Start since it only needs to be calc'd once - ((((partSize.x * partSize.y) * 2) + ((partSize.x * partSize.z) * 2) + ((partSize.y * partSize.z) * 2)) * sizeAdjust); //mass * surface area approximation of a cylinder, where H/W are unknown - if (HighLogic.LoadedSceneIsFlight) //Value correction for loading legacy craft via VesselMover spawner/tournament autospawn that haven't got a armorvolume value in their .craft file. - { - armorVolume *= 0.63f; //part bounds dimensions when calced in Flight are consistantly 1.6-1.7x larger than correct SPH dimensions. Won't be exact, but good enough for legacy craft support - } - if (BDArmorySettings.DRAW_ARMOR_LABELS) Debug.Log("[ARMOR]: part size is (X: " + partSize.x + ";, Y: " + partSize.y + "; Z: " + partSize.z); - if (BDArmorySettings.DRAW_ARMOR_LABELS) Debug.Log("[ARMOR]: size adjust mult: " + sizeAdjust + "; part srf area: " + ((((partSize.x * partSize.y) * 2) + ((partSize.x * partSize.z) * 2) + ((partSize.y * partSize.z) * 2)) * sizeAdjust)); - } - SetupPrefab(); - if (HighLogic.LoadedSceneIsEditor && !isProcWing) - { - var r = part.GetComponentsInChildren(); - { - for (int i = 0; i < r.Length; i++) - { - defaultShader.Add(r[i].material.shader); - if (BDArmorySettings.DRAW_ARMOR_LABELS) Debug.Log("[ARMOR] part shader is " + r[i].material.shader.name); - if (r[i].material.HasProperty("_Color")) - { - defaultColor.Add(r[i].material.color); - } - } - } - } - Armour = Armor; - StartCoroutine(DelayedOnStart()); // Delay updating mass, armour, hull and HP so mods like proc wings and tweakscale get the right values. - // if (HighLogic.LoadedSceneIsFlight) - // { - // if (BDArmorySettings.DRAW_ARMOR_LABELS) Debug.Log("[ARMOR] part mass is: " + (part.mass - armorMass) + "; Armor mass is: " + armorMass + "; hull mass adjust: " + HullmassAdjust + "; total: " + part.mass); - // } - CalculateDryCost(); - } - - IEnumerator DelayedOnStart() - { - yield return null; - if (part == null) yield break; - partMass = part.partInfo.partPrefab.mass; - _updateMass = true; - _armorModified = true; - _hullModified = true; - _updateHitpoints = true; - } - - private void OnDestroy() - { - if (bottom != null) bottom = null; - if (top != null) top = null; - GameEvents.onEditorShipModified.Remove(ShipModified); - GameEvents.onPartDie.Remove(OnPartDie); - } - - void OnPartDie() { OnPartDie(part); } - - void OnPartDie(Part p) - { - if (p == part) - { - Destroy(this); // Force this module to be removed from the gameObject as something is holding onto part references and causing a memory leak. - } - } - - public void ShipModified(ShipConstruct data) - { - // Note: this triggers if the ship is modified, but really we only want to run this when the part is modified. - if (!isProcWing) - { - _updateHitpoints = true; - _updateMass = true; - } - else - { - if (!_delayedShipModifiedRunning) - StartCoroutine(DelayedShipModified()); - } - } - - private bool _delayedShipModifiedRunning = false; - IEnumerator DelayedShipModified() // Wait a frame before triggering to allow proc wings to update it's mass properly. - { - _delayedShipModifiedRunning = true; - yield return null; - _delayedShipModifiedRunning = false; - if (part == null) yield break; - _updateHitpoints = true; - _updateMass = true; - } - - public void ArmorModified(BaseField field, object obj) - { - _armorModified = true; - foreach (var p in part.symmetryCounterparts) - { - var hp = p.GetComponent(); - if (hp == null) continue; - hp._armorModified = true; - } - } - public void HullModified(BaseField field, object obj) - { - _hullModified = true; - foreach (var p in part.symmetryCounterparts) - { - var hp = p.GetComponent(); - if (hp == null) continue; - hp._hullModified = true; - } - } - - public override void OnUpdate() // This only runs in flight mode. - { - if (!_finished_setting_up) return; - RefreshHitPoints(); - if (HighLogic.LoadedSceneIsFlight && !GameIsPaused) - { - if (BDArmorySettings.HEART_BLEED_ENABLED && ShouldHeartBleed()) - { - HeartBleed(); - } - if (ArmorTypeNum > 1 || ArmorPanel) - { - if (part.skinTemperature > SafeUseTemp * 1.5f) - { - ReduceArmor((armorVolume * ((float)part.skinTemperature / SafeUseTemp)) * TimeWarp.fixedDeltaTime); //armor's melting off ship - } - } - } - } - - void Update() // This stops running once things are set up. - { - if (HighLogic.LoadedSceneIsEditor || HighLogic.LoadedSceneIsFlight) // Also needed in flight mode for initial setup of mass, hull and HP, but shouldn't be triggered afterwards as ShipModified is only for the editor. - { - if (_armorModified) - { - _armorModified = false; - ArmorSetup(null, null); - } - if (_hullModified && !_updateMass) // Wait for the mass to update first. - { - _hullModified = false; - HullSetup(null, null); - } - if (!_updateMass) // Wait for the mass to update first. - RefreshHitPoints(); - if (HighLogic.LoadedSceneIsFlight && _armorConfigured && _hullConfigured && _hpConfigured) // No more changes, we're done. - { - _finished_setting_up = true; - enabled = false; - } - } - } - - void FixedUpdate() // This stops running once things are set up. - { - if (_updateMass) - { - _updateMass = false; - var oldPartMass = partMass; - var oldHullMassAdjust = HullMassAdjust; // We need to temporarily remove the HullmassAdjust and update the part.mass to get the correct value as KSP clamps the mass to > 1e-4. - HullMassAdjust = 0; - part.UpdateMass(); - //partMass = part.mass - armorMass - HullMassAdjust; //part mass is taken from the part.cfg val, not current part mass; this overrides that - //need to get ModuleSelfSealingTank mass adjustment. Could move the SST module to BDA.Core - if (isProcWing) - { - float Safetymass = 0; - if (part.Modules.Contains("ModuleSelfSealingTank")) - { - var SST = part.Modules["ModuleSelfSealingTank"]; - // Safetymass = SST.Fields["FBmass"].GetValue(SST) + SST.Fields["FISmass"].GetValue(SST); // SST.Fields["FBmass"] and SST.Fields["FISmass] are both null. - Safetymass = SST.Fields.GetValue("FBmass") + SST.Fields.GetValue("FISmass"); - } - partMass = part.mass - armorMass - HullMassAdjust - Safetymass; - } - CalculateDryCost(); //recalc if modify event added a fueltank -resource swap, etc - HullMassAdjust = oldHullMassAdjust; // Put the HullmassAdjust back so we can test against it when we update the hull mass. - if (oldPartMass != partMass) - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log($"[BDArmory.HitpointTracker]: {part.name} updated mass at {Time.time}: part.mass {part.mass}, partMass {oldPartMass}->{partMass}, armorMass {armorMass}, hullMassAdjust {HullMassAdjust}"); - _hullModified = true; // Modifying the mass modifies the hull. - _updateHitpoints = true; - } - } - } - - private void RefreshHitPoints() - { - if (_updateHitpoints) - { - _updateHitpoints = false; - _forceUpdateHitpointsUI = false; - SetupPrefab(); - } - } - - #region HeartBleed - private bool ShouldHeartBleed() - { - // wait until "now" exceeds the "next tick" value - double dTime = Planetarium.GetUniversalTime(); - if (dTime < nextHeartBleedTime) - { - //Debug.Log(string.Format("[HitpointTracker] TimeSkip ShouldHeartBleed for {0} on {1}", part.name, part.vessel.vesselName)); - return false; - } - - // assign next tick time - double interval = BDArmorySettings.HEART_BLEED_INTERVAL; - nextHeartBleedTime = dTime + interval; - - return true; - } - - private void HeartBleed() - { - float rate = BDArmorySettings.HEART_BLEED_RATE; - float deduction = Hitpoints * rate; - if (Hitpoints - deduction < BDArmorySettings.HEART_BLEED_THRESHOLD) - { - // can't die from heart bleed - return; - } - // deduct hp base on the rate - //Debug.Log(string.Format("[HitpointTracker] Heart bleed {0} on {1} by {2:#.##} ({3:#.##}%)", part.name, part.vessel.vesselName, deduction, rate*100.0)); - AddDamage(deduction); - } - #endregion - - #region Hitpoints Functions - - public float CalculateTotalHitpoints() - { - float hitpoints; - - if (!part.IsMissile()) - { - if (!ArmorPanel) - { - if (maxHitPoints <= 0) - { - var averageSize = part.GetAverageBoundSize(); - var sphereRadius = averageSize * 0.5f; - var sphereSurface = 4 * Mathf.PI * sphereRadius * sphereRadius; - var thickness = 0.1f;// * part.GetTweakScaleMultiplier(); // Tweakscale scales mass as r^3 insted of 0.1*r^2, however it doesn't take the increased volume of the hull into account when scaling resource amounts. - var structuralVolume = Mathf.Max(sphereSurface * thickness, 1e-3f); // Prevent 0 volume, just in case. structural volume is 10cm * surface area of equivalent sphere. - bool clampHP = false; - - var density = (partMass * 1000f) / structuralVolume; - if (density > 1e5f || density < 10) - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log($"[BDArmory.HitpointTracker]: {part.name} extreme density detected: {density}! Trying alternate approach based on partSize."); - structuralVolume = (partSize.x * partSize.y + partSize.x * partSize.z + partSize.y * partSize.z) * 2f * sizeAdjust * Mathf.PI / 6f * 0.1f; // Box area * sphere/cube ratio * 10cm. We use sphere/cube ratio to get similar results as part.GetAverageBoundSize(). - density = (partMass * 1000f) / structuralVolume; - if (density > 1e5f || density < 10) - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log($"[BDArmory.HitpointTracker]: {part.name} still has extreme density: {density}! Setting HP based only on mass instead."); - clampHP = true; - } - } - density = Mathf.Clamp(density, 1000, 10000); - //if (BDArmorySettings.DRAW_DEBUG_LABELS) - //Debug.Log("[BDArmory.HitpointTracker]: Hitpoint Calc" + part.name + " | structuralVolume : " + structuralVolume); - // if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.HitpointTracker]: Hitpoint Calc" + part.name + " | Density : " + density); - - var structuralMass = density * structuralVolume; //this just means hp = mass if the density is within the limits. - - //biger things need more hp; but things that are denser, should also have more hp, so it's a bit mroe complicated than have hp = volume * hp mult - //hp = (volume * Hp mult) * density mod? - //lets take some examples; 3 identical size parts, mk1 cockpit(930kg), mk1 stuct tube (100kg), mk1 LF tank (250kg) - //if, say, a Hp mod of 300, so 2.55m3 * 300 = 765 -> 800hp - //cockpit has a density of ~364, fueltank of 98, struct tube of 39 - //density can't be linear scalar. Cuberoot? would need to reduce hp mult. - //2.55 * 100* 364^1/3 = 1785, 2.55 * 100 * 98^1/3 = 1157, 2.55 * 100 * 39^1/3 = 854 - - // if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.HitpointTracker]: " + part.name + " structural Volume: " + structuralVolume + "; density: " + density); - //3. final calculations - hitpoints = structuralMass * hitpointMultiplier * 0.333f; - //hitpoints = (structuralVolume * Mathf.Pow(density, .333f) * Mathf.Clamp(80 - (structuralVolume / 2), 80 / 4, 80)) * hitpointMultiplier * 0.333f; //volume * cuberoot of density * HP mult scaled by size - if (clampHP) - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log($"[BDArmory.HitpointTracker]: Clamping hitpoints for part {part.name} from {hitpoints} to {hitpointMultiplier * (partMass + HullMassAdjust) * 333f}"); - hitpoints = hitpointMultiplier * partMass * 333f; - } - // SuicidalInsanity B9 patch //should this come before the hp clamping? - if (isProcWing) - { - if (part.Modules.Contains("FARWingAerodynamicModel") || part.Modules.Contains("FARControllableSurface")) - { - //procwing hp already modified by mass, because it is mass - //so using base part mass is it can be properly modified my material HP mod below - hitpoints = (partMass * 1000f) * 3.5f * hitpointMultiplier * 0.333f; //To account for FAR's Strength-mass Scalar. - //unfortunately the same trick can't be used for FAR wings, so mass hack it is. - } - else - { - //hitpoints = (partMass * 1000f) * 7f * hitpointMultiplier * 0.333f; // since wings are basically a 2d object, lets have mass be our scalar - afterall, 2x the mass will ~= 2x the surfce area - hitpoints = (float)Math.Round(part.Modules.GetModule().deflectionLiftCoeff, 2) * 700 * hitpointMultiplier * 0.333f; //this yields the same result, but not beholden to mass changes - } //breaks when pWings are made stupidly thick/large //should really figure out a fix for that someday - - } - - switch (HullTypeNum) - { - case 1: - hitpoints /= 4; - break; - case 3: - hitpoints *= 1.75f; - break; - } - hitpoints = Mathf.Round(hitpoints / HpRounding) * HpRounding; - if (hitpoints <= 0) hitpoints = HpRounding; - if (BDArmorySettings.DRAW_DEBUG_LABELS && maxHitPoints <= 0 && Hitpoints != hitpoints) Debug.Log($"[BDArmory.HitpointTracker]: {part.name} updated HP: {Hitpoints}->{hitpoints} at time {Time.time}, partMass: {partMass}, density: {density}, structuralVolume: {structuralVolume}, structuralMass {structuralMass}"); - } - else // Override based on part configuration for custom parts - { - switch (HullTypeNum) - { - case 1: - hitpoints = maxHitPoints / 4; - break; - case 3: - hitpoints = maxHitPoints * 1.75f; - break; - default: - hitpoints = maxHitPoints; - break; - } - hitpoints = Mathf.Round(hitpoints / HpRounding) * HpRounding; - if (hitpoints <= 0) hitpoints = HpRounding; - if (BDArmorySettings.DRAW_DEBUG_LABELS && maxHitPoints <= 0 && Hitpoints != hitpoints) Debug.Log($"[BDArmory.HitpointTracker]: {part.name} updated HP: {Hitpoints}->{hitpoints} at time {Time.time}"); - } - } - else - { - hitpoints = ArmorRemaining; // * armorVolume * 10; - //hitpoints = Mathf.Round(hitpoints / HpRounding) * HpRounding; - //armorpanel HP is panel integrity, as 'HP' is the slab of armor; having a secondary unused HP pool will only make armor massively more effective against explosions than it should due to how isInLineOfSight calculates intermediate parts - } - } - else - { - hitpoints = 5; - Armor = 2; - } - if (hitpoints <= 0) hitpoints = HpRounding; - if (!_finished_setting_up && _armorConfigured && _hullConfigured) _hpConfigured = true; - return hitpoints; - } - - public void DestroyPart() - { - if ((part.mass - armorMass) <= 2f) part.explosionPotential *= 0.85f; - - PartExploderSystem.AddPartToExplode(part); - } - - public float GetMaxArmor() - { - UI_FloatRange armorField = (UI_FloatRange)Fields["Armor"].uiControlEditor; - return armorField.maxValue; - } - - public float GetMaxHitpoints() - { - UI_ProgressBar hitpointField = (UI_ProgressBar)Fields["Hitpoints"].uiControlEditor; - return hitpointField.maxValue; - } - - public bool GetFireFX() - { - return FireFX; - } - - public void SetDamage(float partdamage) - { - Hitpoints = partdamage; //given the sole reference is from destroy, with damage = -1, shouldn't this be =, not -=? - - if (Hitpoints <= 0) - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.HitPointTracker] Setting HP to " + Hitpoints + ", destroying"); - DestroyPart(); - } - } - - public void AddDamage(float partdamage, bool overcharge = false) - { - if (isAI) return; - if (ArmorPanel) - { - if (BDArmorySettings.DRAW_ARMOR_LABELS) Debug.Log("[BDArmory.HitPointTracker] AddDamage(), hit part is armor panel, returning"); - return; - } - - partdamage = Mathf.Max(partdamage, 0f) * -1; - Hitpoints += (partdamage / defenseMutator); //why not just go -= partdamage? - if (Hitpoints <= 0) - { - DestroyPart(); - } - } - - public void AddHealth(float partheal, bool overcharge = false) - { - if (isAI) return; - if (Hitpoints + partheal < BDArmorySettings.HEART_BLEED_THRESHOLD) //in case of negative regen value (for HP drain) - { - return; - } - Hitpoints += partheal; - - Hitpoints = Mathf.Clamp(Hitpoints, -1, overcharge ? Mathf.Min(previousHitpoints * 2, previousHitpoints + 1000) : previousHitpoints); //Allow vampirism to overcharge HP - } - - public void AddDamageToKerbal(KerbalEVA kerbal, float damage) - { - damage = Mathf.Max(damage, 0f) * -1; - Hitpoints += damage; - - if (Hitpoints <= 0) - { - // oh the humanity! - PartExploderSystem.AddPartToExplode(kerbal.part); - } - } - #endregion Hitpoints Functions - - #region Armor - - public void ReduceArmor(float massToReduce) //incoming massToreduce should be cm3 - { - if (BDArmorySettings.DRAW_ARMOR_LABELS) - { - Debug.Log("[HPTracker] armor mass: " + armorMass + "; mass to reduce: " + (massToReduce * Math.Round((Density / 1000000), 3)) + "kg"); //g/m3 - } - float reduceMass = (massToReduce * (Density / 1000000000)); //g/cm3 conversion to yield tons - if (armorMass > 0) - { - Armor -= ((reduceMass * 2) / armorMass) * Armor; //armor that's 50% air isn't going to stop anything and could be considered 'destroyed' so lets reflect that by doubling armor loss (this will also nerf armor panels from 'god-tier' to merely 'very very good' - if (Armor < 0) - { - Armor = 0; - ArmorRemaining = 0; - } - else ArmorRemaining = Armor / StartingArmor * 100; - Armour = Armor; - } - else - { - if (Armor < 0) - { - Armor = 0; - ArmorRemaining = 0; - Armour = Armor; - } - } - if (ArmorPanel) - { - Hitpoints = ArmorRemaining; // * armorVolume * 10; - if (Armor <= 0) - { - DestroyPart(); - } - } - armorMass -= reduceMass; //tons - if (armorMass <= 0) - { - armorMass = 0; - } - } - - public void overrideArmorSetFromConfig() - { - ArmorSet = true; - if (ArmorThickness > 10 || ArmorPanel) //primarily panels, but any thing that starts with more than default armor - { - startsArmored = true; - if (Armor > 10 && Armor != ArmorThickness) //if settings modified and loading in from craft fiel - { } - else - { - Armor = ArmorThickness; - } - /* - UI_FloatRange armortypes = (UI_FloatRange)Fields["ArmorTypeNum"].uiControlEditor; - armortypes.minValue = 2f; //prevent panels from being switched to "None" armor type - if (ArmorTypeNum == 1) - { - ArmorTypeNum = 2; - } - */ - } - if (maxSupportedArmor < 0) //hasn't been set in cfg - { - if (part.IsAero()) - { - maxSupportedArmor = 20; - } - else - { - maxSupportedArmor = ((partSize.x / 20) * 1000); //~62mm for Size1, 125mm for S2, 185mm for S3 - maxSupportedArmor /= 5; - maxSupportedArmor = Mathf.Round(maxSupportedArmor); - maxSupportedArmor *= 5; - } - if (ArmorThickness > 10 && ArmorThickness > maxSupportedArmor)//part has custom armor value, use that - { - maxSupportedArmor = ArmorThickness; - } - } - if (BDArmorySettings.DRAW_ARMOR_LABELS) - { - Debug.Log("[ARMOR] max supported armor for " + part.name + " is " + maxSupportedArmor); - } - //if maxSupportedArmor > 0 && < armorThickness, that's entirely the fault of the MM patcher - UI_FloatRange armorFieldFlight = (UI_FloatRange)Fields["Armor"].uiControlFlight; - armorFieldFlight.minValue = 0f; - armorFieldFlight.maxValue = maxSupportedArmor; - UI_FloatRange armorFieldEditor = (UI_FloatRange)Fields["Armor"].uiControlEditor; - armorFieldEditor.maxValue = maxSupportedArmor; - armorFieldEditor.minValue = 1f; - armorFieldEditor.onFieldChanged = ArmorModified; - part.RefreshAssociatedWindows(); - } - - public void ArmorSetup(BaseField field, object obj) - { - if (OldArmorType != ArmorTypeNum) - { - if ((ArmorTypeNum - 1) > ArmorInfo.armorNames.Count) //in case of trying to load a craft using a mod armor type that isn't installed and having a armorTypeNum larger than the index size - { - /* - if (startsArmored || ArmorPanel) - { - if (ArmorTypeNum == 1) - { - ArmorTypeNum = 2; //part starts with armor - } - } - else - { - */ - ArmorTypeNum = 1; //reset to 'None' - //} - } - if (isAI || part.IsMissile() || BDArmorySettings.RESET_ARMOUR) - { - ArmorTypeNum = 1; //reset to 'None' - } - armorInfo = ArmorInfo.armors[ArmorInfo.armorNames[(int)ArmorTypeNum - 1]]; //what does this return if armorname cannot be found (mod armor removed/not present in install?) - - //if (SelectedArmorType != ArmorInfo.armorNames[(int)ArmorTypeNum - 1]) //armor selection overridden by Editor widget - //{ - // armorInfo = ArmorInfo.armors[SelectedArmorType]; - // ArmorTypeNum = ArmorInfo.armors.FindIndex(t => t.name == SelectedArmorType); //adjust part's current armor setting to match - //} - guiArmorTypeString = armorInfo.name; - SelectedArmorType = armorInfo.name; - Density = armorInfo.Density; - Diffusivity = armorInfo.Diffusivity; - Ductility = armorInfo.Ductility; - Hardness = armorInfo.Hardness; - Strength = armorInfo.Strength; - SafeUseTemp = armorInfo.SafeUseTemp; - SetArmor(); - } - if (BDArmorySettings.LEGACY_ARMOR) - { - guiArmorTypeString = "Steel"; - SelectedArmorType = "Legacy Armor"; - Density = 7850; - Diffusivity = 48.5f; - Ductility = 0.15f; - Hardness = 1176; - Strength = 940; - SafeUseTemp = 2500; - if (BDArmorySettings.DRAW_ARMOR_LABELS) - { - Debug.Log("[ARMOR] Armor of " + part.name + " reset by LEGACY_ARMOUR"); - } - } - else if (BDArmorySettings.RESET_ARMOUR) //don't reset armor panels - { - guiArmorTypeString = "None"; - SelectedArmorType = "None"; - Density = 2700; - Diffusivity = 237f; - Ductility = 0.6f; - Hardness = 300; - Strength = 200; - SafeUseTemp = 993; - Armor = 10; - if (ArmorPanel) Armor = 25; - if (BDArmorySettings.DRAW_ARMOR_LABELS) - { - Debug.Log("[ARMOR] Armor of " + part.name + " reset to defaults by RESET_ARMOUR"); - } - } - var oldArmorMass = armorMass; - armorMass = 0; - armorCost = 0; - if (ArmorTypeNum > 1 && (!BDArmorySettings.LEGACY_ARMOR || (!BDArmorySettings.RESET_ARMOUR || (BDArmorySettings.RESET_ARMOUR && ArmorThickness > 10)))) //don't apply cost/mass to None armor type - { - armorMass = (Armor / 1000) * armorVolume * Density / 1000; //armor mass in tons - armorCost = (Armor / 1000) * armorVolume * armorInfo.Cost; //armor cost, tons - } - if (ArmorTypeNum == 1 && ArmorPanel) - { - armorMass = (Armor / 1000) * armorVolume * Density / 1000; - guiArmorTypeString = "Aluminium"; - SelectedArmorType = "None"; - armorCost = (Armor / 1000) * armorVolume * armorInfo.Cost; - } - //part.RefreshAssociatedWindows(); //having this fire every time a change happens prevents sliders from being used. Add delay timer? - if (OldArmorType != ArmorTypeNum || oldArmorMass != armorMass) - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log($"[BDArmory.HitpointTracker]: {part.name} updated armour mass {oldArmorMass}->{armorMass} or type {OldArmorType}->{ArmorTypeNum} at time {Time.time}"); - OldArmorType = ArmorTypeNum; - _updateMass = true; - part.UpdateMass(); - if (HighLogic.LoadedSceneIsEditor && EditorLogic.fetch != null) - GameEvents.onEditorShipModified.Fire(EditorLogic.fetch.ship); - } - _armorConfigured = true; - } - - public void SetArmor() - { - //if (isAI) return; //replace with newer implementation - if (BDArmorySettings.LEGACY_ARMOR || BDArmorySettings.RESET_ARMOUR) return; - if (part.IsMissile()) return; - if (ArmorTypeNum > 1 || ArmorPanel) - { - /* - UI_FloatRange armorFieldFlight = (UI_FloatRange)Fields["Armor"].uiControlFlight; - if (armorFieldFlight.maxValue != maxSupportedArmor) - { - armorReset = false; - armorFieldFlight.minValue = 0f; - armorFieldFlight.maxValue = maxSupportedArmor; - } - */ - UI_FloatRange armorFieldEditor = (UI_FloatRange)Fields["Armor"].uiControlEditor; - if (armorFieldEditor.maxValue != maxSupportedArmor) - { - armorReset = false; - armorFieldEditor.maxValue = maxSupportedArmor; - armorFieldEditor.minValue = 1f; - } - armorFieldEditor.onFieldChanged = ArmorModified; - if (!armorReset) - { - part.RefreshAssociatedWindows(); - } - armorReset = true; - } - else - { - Armor = 10; - UI_FloatRange armorFieldEditor = (UI_FloatRange)Fields["Armor"].uiControlEditor; - armorFieldEditor.maxValue = 10; //max none armor to 10 (simulate part skin of alimunium) - armorFieldEditor.minValue = 10; - //UI_FloatRange armorFieldFlight = (UI_FloatRange)Fields["Armor"].uiControlFlight; - //armorFieldFlight.minValue = 0f; - //armorFieldFlight.maxValue = 10; - part.RefreshAssociatedWindows(); - //GameEvents.onEditorShipModified.Fire(EditorLogic.fetch.ship); - } - } - private static Bounds CalcPartBounds(Part p, Transform t) - { - Bounds result = new Bounds(t.position, Vector3.zero); - Bounds[] bounds = p.GetRendererBounds(); //slower than getColliderBounds, but it only runs once, and doesn't have to deal with culling isTrgger colliders (airlocks, ladders, etc) - //Err... not so sure about that, me. This is yielding different resutls in SPH/flight. SPH is proper dimensions, flight is giving bigger x/y/z - // a mk1 cockpit (x: 1.25, y: 1.6, z: 1.9, area 11 in SPh becomes x: 2.5, y: 1.25, z: 2.5, area 19 - { - if (!p.Modules.Contains("LaunchClamp")) - { - for (int i = 0; i < bounds.Length; i++) - { - result.Encapsulate(bounds[i]); - } - } - } - return result; - } - - public void HullSetup(BaseField field, object obj) //no longer needed for realtime HP calcs, but does need to be updated occasionally to give correct vessel mass - { - if (isProcWing) - { - StartCoroutine(WaitForHullSetup()); - } - else - { - SetHullMass(); - } - } - IEnumerator WaitForHullSetup() - { - if (waitingForHullSetup) yield break; // Already waiting. - waitingForHullSetup = true; - yield return null; - waitingForHullSetup = false; - if (part == null) yield break; // The part disappeared! - - SetHullMass(); - } - void SetHullMass() - { - if (IgnoreForArmorSetup) return; - if (isAI || ArmorPanel || BDArmorySettings.RESET_HULL || BDArmorySettings.LEGACY_ARMOR) HullTypeNum = 2; - if ((part.isEngine() || part.IsWeapon()) && HullTypeNum < 2) //can armor engines, but not make them out of wood. - { - HullTypeNum = 2; - } - var OldHullMassAdjust = HullMassAdjust; - if (HullTypeNum == 1) - { - HullMassAdjust = partMass / 3 - partMass; - guiHullTypeString = Localizer.Format("#LOC_BDArmory_Wood"); - part.maxTemp = 770; - HullCostAdjust = Mathf.Max(((part.partInfo.cost + part.partInfo.variant.Cost) - (float)resourceCost) / 2, (part.partInfo.cost + part.partInfo.variant.Cost) - 500) - ((part.partInfo.cost + part.partInfo.variant.Cost) - (float)resourceCost);//make wooden parts up to 500 funds cheaper - //this returns cost of base variant, yielding part vairaints that are discounted by 50% or 500 of base varaint cost, not current variant. method to get currently selected variant? - } - else if (HullTypeNum == 2) - { - HullMassAdjust = 0; - guiHullTypeString = Localizer.Format("#LOC_BDArmory_Aluminium"); - //removing maxtemp from aluminium and steel to prevent hull type from causing issues with, say, spacecraft re-entry on installs with BDA not used exclusively for BDA - HullCostAdjust = 0; - } - else //hulltype 3 - { - HullMassAdjust = partMass; - guiHullTypeString = Localizer.Format("#LOC_BDArmory_Steel"); - HullCostAdjust = Mathf.Min(((part.partInfo.cost + part.partInfo.variant.Cost) - (float)resourceCost) * 2, ((part.partInfo.cost + part.partInfo.variant.Cost) - (float)resourceCost) + 1500); //make steel parts rather more expensive - } - if (OldHullType != HullTypeNum || OldHullMassAdjust != HullMassAdjust) - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log($"[BDArmory.HitpointTracker]: {part.name} updated hull mass {OldHullMassAdjust}->{HullMassAdjust} (part mass {partMass}, total mass {part.mass + HullMassAdjust - OldHullMassAdjust}) or type {OldHullType}->{HullTypeNum} at time {Time.time}"); - OldHullType = HullTypeNum; - _updateMass = true; - part.UpdateMass(); - if (HighLogic.LoadedSceneIsEditor && EditorLogic.fetch != null) - GameEvents.onEditorShipModified.Fire(EditorLogic.fetch.ship); - } - _hullConfigured = true; - } - private List GetResources() - { - List resources = new List(); - - foreach (PartResource resource in part.Resources) - { - if (!resources.Contains(resource)) { resources.Add(resource); } - } - return resources; - } - private void CalculateDryCost() - { - resourceCost = 0; - foreach (PartResource resource in GetResources()) - { - var resources = part.Resources.ToList(); - using (IEnumerator res = resources.GetEnumerator()) - while (res.MoveNext()) - { - if (res.Current == null) continue; - if (res.Current.resourceName == resource.resourceName) - { - resourceCost += res.Current.info.unitCost * res.Current.maxAmount; //turns out parts subtract res cost even if the tank starts empty - } - } - } - } - #endregion Armor - public override string GetInfo() - { - StringBuilder output = new StringBuilder(); - output.Append(Environment.NewLine); - if (startsArmored || ArmorPanel) - { - output.AppendLine($"Starts Armored"); - output.AppendLine($" - Armor Mass: {armorMass}"); - } - return output.ToString(); - } - } -} diff --git a/BDArmory.Core/PartExploderSystem.cs b/BDArmory.Core/PartExploderSystem.cs deleted file mode 100644 index a1ea9e198..000000000 --- a/BDArmory.Core/PartExploderSystem.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Collections.Generic; -using UnityEngine; - -namespace BDArmory.Core -{ - [KSPAddon(KSPAddon.Startup.Flight, false)] - public class PartExploderSystem : MonoBehaviour - { - private static readonly Queue ExplodingPartsQueue = new Queue(); - - public static void AddPartToExplode(Part p) - { - if (p != null && !ExplodingPartsQueue.Contains(p)) - { - ExplodingPartsQueue.Enqueue(p); - } - } - - private void OnDestroy() - { - ExplodingPartsQueue.Clear(); - } - - public void Update() - { - if (ExplodingPartsQueue.Count == 0) return; - - do - { - Part part = ExplodingPartsQueue.Dequeue(); - - if (part != null) - { - part.explode(); // This calls part.Die() internally. - } - } while (ExplodingPartsQueue.Count > 0); - } - } -} diff --git a/BDArmory.Core/Properties/AssemblyInfo.cs b/BDArmory.Core/Properties/AssemblyInfo.cs deleted file mode 100644 index b55e8b544..000000000 --- a/BDArmory.Core/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("BDArmory.Core")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("")] -[assembly: AssemblyCopyright("")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("a6f1753e-9570-4c40-af72-a179890582e5")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.4.18.4")] -[assembly: AssemblyFileVersion("1.4.18.4")] -[assembly: KSPAssembly("BDArmory.Core", 1, 0)] diff --git a/BDArmory.Core/Utils/BDAMath.cs b/BDArmory.Core/Utils/BDAMath.cs deleted file mode 100644 index b3b764a5a..000000000 --- a/BDArmory.Core/Utils/BDAMath.cs +++ /dev/null @@ -1,36 +0,0 @@ -namespace BDArmory.Core.Utils -{ - public static class BDAMath - { - public static float RangedProbability(float[] probs) - { - float total = 0; - foreach (float elem in probs) - { - total += elem; - } - - float randomPoint = UnityEngine.Random.value * total; - - for (int i = 0; i < probs.Length; i++) - { - if (randomPoint < probs[i]) - { - return i; - } - else - { - randomPoint -= probs[i]; - } - } - return probs.Length - 1; - } - - public static bool Between(this float num, float lower, float upper, bool inclusive = true) - { - return inclusive - ? lower <= num && num <= upper - : lower < num && num < upper; - } - } -} diff --git a/BDArmory/.ksplocalizer.settings.user b/BDArmory/.ksplocalizer.settings.user deleted file mode 100644 index 2162598d0..000000000 --- a/BDArmory/.ksplocalizer.settings.user +++ /dev/null @@ -1,4 +0,0 @@ - - - 6000000 - \ No newline at end of file diff --git a/BDArmory/Bullets/BulletInfo.cs b/BDArmory/Ammo/BulletInfo.cs similarity index 92% rename from BDArmory/Bullets/BulletInfo.cs rename to BDArmory/Ammo/BulletInfo.cs index e3781fa10..286163a09 100644 --- a/BDArmory/Bullets/BulletInfo.cs +++ b/BDArmory/Ammo/BulletInfo.cs @@ -12,7 +12,7 @@ public class BulletInfo public float caliber { get; private set; } public float bulletMass { get; private set; } public float bulletVelocity { get; private set; } - public bool explosive { get; private set; } //left for legacy support + public string explosive { get; private set; } //left for legacy support public bool incendiary { get; private set; } //left for legacy support //public string attributeTags { get; private set; } //replace this with a string? tags to add: HE, incendiary, EMP, nuclear, beehive, homing, massmod, impulse; //nuclear can use tntmass for kT, beehive can use submunition#, would need submunition bulletType, homing would need degrees/s, massmod needs mass mod, impulse needs impulse @@ -25,6 +25,7 @@ public class BulletInfo public float impulse { get; private set; } public string fuzeType { get; private set; } public int subProjectileCount { get; private set; } + public float subProjectileDispersion { get; private set; } public float apBulletMod { get; private set; } public string bulletDragTypeName { get; private set; } public string projectileColor { get; private set; } @@ -36,8 +37,8 @@ public class BulletInfo public static BulletInfo defaultBullet; public BulletInfo(string name, string DisplayName, float caliber, float bulletVelocity, float bulletMass, - bool explosive, bool incendiary, float tntMass, bool EMP, bool nuclear, bool beehive, string subMunitionType, float massMod, float impulse, string fuzeType, float apBulletDmg, - int subProjectileCount, string bulletDragTypeName, string projectileColor, string startColor, bool fadeColor) + string explosive, bool incendiary, float tntMass, bool EMP, bool nuclear, bool beehive, string subMunitionType, float massMod, float impulse, string fuzeType, float apBulletDmg, + int subProjectileCount, float subProjectileDispersion, string bulletDragTypeName, string projectileColor, string startColor, bool fadeColor) { this.name = name; this.DisplayName = DisplayName; @@ -56,6 +57,7 @@ public BulletInfo(string name, string DisplayName, float caliber, float bulletVe this.fuzeType = fuzeType; this.apBulletMod = apBulletDmg; this.subProjectileCount = subProjectileCount; + this.subProjectileDispersion = subProjectileDispersion; this.bulletDragTypeName = bulletDragTypeName; this.projectileColor = projectileColor; this.startColor = startColor; @@ -85,7 +87,7 @@ public static void Load() (float)ParseField(node, "caliber", typeof(float)), (float)ParseField(node, "bulletVelocity", typeof(float)), (float)ParseField(node, "bulletMass", typeof(float)), - (bool)ParseField(node, "explosive", typeof(bool)), + (string)ParseField(node, "explosive", typeof(string)), (bool)ParseField(node, "incendiary", typeof(bool)), (float)ParseField(node, "tntMass", typeof(float)), (bool)ParseField(node, "EMP", typeof(bool)), @@ -97,6 +99,7 @@ public static void Load() (string)ParseField(node, "fuzeType", typeof(string)), (float)ParseField(node, "apBulletMod", typeof(float)), Math.Max((int)ParseField(node, "subProjectileCount", typeof(int)), 1), + -1, (string)ParseField(node, "bulletDragTypeName", typeof(string)), (string)ParseField(node, "projectileColor", typeof(string)), (string)ParseField(node, "startColor", typeof(string)), @@ -130,7 +133,7 @@ public static void Load() (float)ParseField(node, "caliber", typeof(float)), (float)ParseField(node, "bulletVelocity", typeof(float)), (float)ParseField(node, "bulletMass", typeof(float)), - (bool)ParseField(node, "explosive", typeof(bool)), + (string)ParseField(node, "explosive", typeof(string)), (bool)ParseField(node, "incendiary", typeof(bool)), (float)ParseField(node, "tntMass", typeof(float)), (bool)ParseField(node, "EMP", typeof(bool)), @@ -142,6 +145,7 @@ public static void Load() (string)ParseField(node, "fuzeType", typeof(string)), (float)ParseField(node, "apBulletMod", typeof(float)), (int)ParseField(node, "subProjectileCount", typeof(int)), + (float)ParseField(node, "subProjectileDispersion", typeof(float)), (string)ParseField(node, "bulletDragTypeName", typeof(string)), (string)ParseField(node, "projectileColor", typeof(string)), (string)ParseField(node, "startColor", typeof(string)), @@ -188,7 +192,7 @@ private static object ParseField(ConfigNode node, string field, Type type) // Give a warning about the missing or invalid value, then use the default value using reflection to find the field. if (field == "DisplayName") return string.Empty; var defaultValue = typeof(BulletInfo).GetProperty(field == "DisplayName" ? "name" : field, BindingFlags.Public | BindingFlags.Instance).GetValue(defaultBullet); //this is returnin the def bullet name, not current bullet name - if (field == "EMP" || field == "nuclear" || field == "beehive" || field == "subMunitionType" || field == "massMod" || field == "impulse") + if (field == "EMP" || field == "nuclear" || field == "beehive" || field == "subMunitionType" || field == "massMod" || field == "impulse" || field == "subProjectileDispersion") { //not having these throw an error message since these are all optional and default to false, prevents bullet defs from bloating like rockets did //Future SI - apply this to rocket, mutator defs diff --git a/BDArmory/Modules/ModuleAmmoSwitch.cs b/BDArmory/Ammo/ModuleAmmoSwitch.cs similarity index 99% rename from BDArmory/Modules/ModuleAmmoSwitch.cs rename to BDArmory/Ammo/ModuleAmmoSwitch.cs index 978667387..afa67381d 100644 --- a/BDArmory/Modules/ModuleAmmoSwitch.cs +++ b/BDArmory/Ammo/ModuleAmmoSwitch.cs @@ -1,10 +1,11 @@ using System; using System.Collections.Generic; using System.Text; -using BDArmory.Misc; using UnityEngine; -namespace BDArmory.Modules +using BDArmory.Utils; + +namespace BDArmory.Ammo { public class ModuleAmmoSwitch : PartModule, IPartCostModifier { diff --git a/BDArmory/Modules/ModuleCASE.cs b/BDArmory/Ammo/ModuleCASE.cs similarity index 88% rename from BDArmory/Modules/ModuleCASE.cs rename to BDArmory/Ammo/ModuleCASE.cs index 1c0e42080..f21610b8b 100644 --- a/BDArmory/Modules/ModuleCASE.cs +++ b/BDArmory/Ammo/ModuleCASE.cs @@ -1,17 +1,18 @@ -using BDArmory.Competition; -using BDArmory.Competition.VesselSpawning; -using BDArmory.Core; -using BDArmory.Core.Extension; -using BDArmory.Core.Utils; -using BDArmory.FX; -using BDArmory.Misc; using System; using System.Text; using System.Collections.Generic; using UniLinq; using UnityEngine; -namespace BDArmory.Modules +using BDArmory.Competition; +using BDArmory.Extensions; +using BDArmory.FX; +using BDArmory.Settings; +using BDArmory.Utils; +using BDArmory.VesselSpawning; +using BDArmory.Weapons; + +namespace BDArmory.Ammo { class ModuleCASE : PartModule, IPartMassModifier, IPartCostModifier { @@ -33,7 +34,7 @@ class ModuleCASE : PartModule, IPartMassModifier, IPartCostModifier public string SourceVessel = ""; public bool hasDetonated = false; private float blastRadius = 0; - int explosionLayerMask = (int)(LayerMasks.Parts | LayerMasks.Scenery | LayerMasks.EVA | LayerMasks.Unknown19 | LayerMasks.Unknown23); + const int explosionLayerMask = (int)(LayerMasks.Parts | LayerMasks.Scenery | LayerMasks.EVA | LayerMasks.Unknown19 | LayerMasks.Unknown23 | LayerMasks.Wheels); public bool externallyCalled = false; @@ -61,6 +62,9 @@ public override void OnStart(StartState state) public bool Case2 = false; private List resourceAmount = new List(); + + static RaycastHit[] raycastHitBuffer = new RaycastHit[10]; // This gets enlarged as needed and is shared amongst all ModuleCASE instances. + public void Start() { if (HighLogic.LoadedSceneIsEditor) @@ -130,7 +134,7 @@ void CASESetup(BaseField field, object obj) while (resource.MoveNext()) { if (resource.Current == null) continue; - resource.Current.maxAmount *= 0.8; + resource.Current.maxAmount *= 0.8; resource.Current.maxAmount = Math.Floor(resource.Current.maxAmount); resource.Current.amount = Math.Min(resource.Current.amount, resource.Current.maxAmount); } @@ -169,10 +173,10 @@ void CASESetup(BaseField field, object obj) } CASE.Case2 = CASE.CASELevel == 2 ? true : false; CASE.externallyCalled = false; - Utils.RefreshAssociatedWindows(pSym.Current); + GUIUtils.RefreshAssociatedWindows(pSym.Current); } Case2 = CASELevel == 2 ? true : false; - Utils.RefreshAssociatedWindows(part); + GUIUtils.RefreshAssociatedWindows(part); } public override void OnLoad(ConfigNode node) { @@ -233,24 +237,30 @@ public void DetonateIfPossible() if (CASELevel == 0) { ExplosionFx.CreateExplosion(part.transform.position, (float)ammoExplosionYield, explModelPath, explSoundPath, ExplosionSourceType.BattleDamage, 120, part, SourceVessel, "Ammunition (CASE-0)", direction, -1, false, part.mass + ((float)ammoExplosionYield * 10f), 1200 * BDArmorySettings.EXP_DMG_MOD_BATTLE_DAMAGE); - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.ModuleCASE] CASE 0 explosion, tntMassEquivilent: " + ammoExplosionYield); + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log("[BDArmory.ModuleCASE]: CASE 0 explosion, tntMassEquivilent: " + ammoExplosionYield); } else { direction = part.transform.up; ExplosionFx.CreateExplosion(part.transform.position, ((float)ammoExplosionYield / 2), limitEdexploModelPath, explSoundPath, ExplosionSourceType.BattleDamage, 60, part, SourceVessel, "Ammunition (CASE-I)", direction, -1, false, part.mass + ((float)ammoExplosionYield * 10f), 600 * BDArmorySettings.EXP_DMG_MOD_BATTLE_DAMAGE); - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.ModuleCASE] CASE I explosion, tntMassEquivilent: " + ammoExplosionYield + ", part: " + part + ", vessel: " + vesselName); + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log("[BDArmory.ModuleCASE]: CASE I explosion, tntMassEquivilent: " + ammoExplosionYield + ", part: " + part + ", vessel: " + vesselName); } } else //if (CASELevel == 2) //blast contained, shunted out side of hull, minimal damage { ExplosionFx.CreateExplosion(part.transform.position, (float)ammoExplosionYield / 4f, shuntExploModelPath, explSoundPath, ExplosionSourceType.BattleDamage, 30, part, SourceVessel, "Ammunition (CASE-II)", direction, -1, true); - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.ModuleCASE] CASE II explosion, tntMassEquivilent: " + ammoExplosionYield); + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log("[BDArmory.ModuleCASE]: CASE II explosion, tntMassEquivilent: " + ammoExplosionYield); Ray BlastRay = new Ray(part.transform.position, part.transform.up); - var hits = Physics.RaycastAll(BlastRay, blastRadius, explosionLayerMask); - if (hits.Length > 0) + var hitCount = Physics.RaycastNonAlloc(BlastRay, raycastHitBuffer, blastRadius, explosionLayerMask); + if (hitCount == raycastHitBuffer.Length) // If there's a whole bunch of stuff in the way (unlikely), then we need to increase the size of our hits buffer. { - var orderedHits = hits.OrderBy(x => x.distance); + raycastHitBuffer = Physics.RaycastAll(BlastRay, blastRadius, explosionLayerMask); + hitCount = raycastHitBuffer.Length; + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log($"[BDArmory.ModuleCASE]: Enlarging hit raycast buffer size to {hitCount}."); + } + if (hitCount > 0) + { + var orderedHits = raycastHitBuffer.Take(hitCount).OrderBy(x => x.distance); using (var hitsEnu = orderedHits.GetEnumerator()) { while (hitsEnu.MoveNext()) @@ -286,11 +296,11 @@ public void DetonateIfPossible() if (hitPart.vessel != part.vessel) { - Vector3 dist = part.transform.position - hitPart.transform.position; + float dist = (part.transform.position - hitPart.transform.position).magnitude; Ray LoSRay = new Ray(part.transform.position, hitPart.transform.position - part.transform.position); RaycastHit LOShit; - if (Physics.Raycast(LoSRay, out LOShit, dist.magnitude, explosionLayerMask)) + if (Physics.Raycast(LoSRay, out LOShit, dist, explosionLayerMask)) { if (FlightGlobals.currentMainBody == null || LOShit.collider.gameObject != FlightGlobals.currentMainBody.gameObject) { @@ -298,7 +308,7 @@ public void DetonateIfPossible() Part p = eva ? eva.part : LOShit.collider.gameObject.GetComponentInParent(); if (p == hitPart) { - ProjectileUtils.CalculateShrapnelDamage(hitPart, hit, 200, (float)ammoExplosionYield, dist.magnitude, this.part.vessel.GetName(), ExplosionSourceType.BattleDamage, part.mass); + ProjectileUtils.CalculateShrapnelDamage(hitPart, hit, 200, (float)ammoExplosionYield, dist, this.part.vessel.GetName(), ExplosionSourceType.BattleDamage, part.mass); } } } @@ -334,7 +344,7 @@ private void ApplyDamage(Part hitPart, RaycastHit hit) float armorToReduce = hitPart.GetArmorThickness() * 0.25f; hitPart.ReduceArmor(armorToReduce); - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.ModuleCASE]" + hitPart.name + " damaged, armor reduced by " + armorToReduce); + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log("[BDArmory.ModuleCASE]: " + hitPart.name + " damaged, armor reduced by " + armorToReduce); BDACompetitionMode.Instance.Scores.RegisterBattleDamage(SourceVessel, hitPart.vessel, explDamage); } @@ -366,12 +376,13 @@ public override string GetInfo() return output.ToString(); } - void Update() + void FixedUpdate() { if (HighLogic.LoadedSceneIsFlight && FlightGlobals.ready && !vessel.packed) { if (BDArmorySettings.BD_FIRES_ENABLED && BDArmorySettings.BD_FIRE_HEATDMG) { + if (hasDetonated) return; if (this.part.temperature > 900) //ammo cooks off, part is too hot { if (!hasDetonated) DetonateIfPossible(); @@ -381,4 +392,3 @@ void Update() } } } - diff --git a/BDArmory/Bullets/PooledBullet.cs b/BDArmory/Ammo/PooledBullet.cs similarity index 66% rename from BDArmory/Bullets/PooledBullet.cs rename to BDArmory/Ammo/PooledBullet.cs index 1d94156cf..ecfd62dd7 100644 --- a/BDArmory/Bullets/PooledBullet.cs +++ b/BDArmory/Ammo/PooledBullet.cs @@ -2,18 +2,20 @@ using System.Collections; using System.Collections.Generic; using System.Linq; -using BDArmory.Core; -using BDArmory.Core.Utils; -using BDArmory.Core.Extension; -using BDArmory.FX; -using BDArmory.Parts; -using BDArmory.Shaders; -using BDArmory.Competition; using UnityEngine; -using BDArmory.Misc; + +using BDArmory.Armor; +using BDArmory.Competition; +using BDArmory.Damage; +using BDArmory.Extensions; +using BDArmory.FX; using BDArmory.Modules; -using BDArmory.Core.Module; +using BDArmory.Settings; +using BDArmory.Shaders; +using BDArmory.Targeting; using BDArmory.UI; +using BDArmory.Utils; +using BDArmory.Weapons; namespace BDArmory.Bullets { @@ -24,11 +26,12 @@ public class PooledBullet : MonoBehaviour public BulletInfo bullet; //public float leftPenetration; //Not used by anything? Was this to provide a upper cap to how far a bullet could pen? - //public enum PooledBulletTypes //this isn't actually used by anything? - //{ - // Standard, - // Explosive - //} + public enum PooledBulletTypes + { + Slug, + Explosive, + Shaped + } public enum BulletFuzeTypes { None, @@ -48,13 +51,14 @@ public enum BulletDragTypes //public PooledBulletTypes bulletType; public BulletFuzeTypes fuzeType; + public PooledBulletTypes HEType; public BulletDragTypes dragType; public Vessel sourceVessel; public string sourceVesselName; public Part sourceWeapon; public string team; - public Color lightColor = Utils.ParseColor255("255, 235, 145, 255"); + public Color lightColor = GUIUtils.ParseColor255("255, 235, 145, 255"); public Color projectileColor; public string bulletTexturePath; public bool fadeColor; @@ -76,7 +80,6 @@ public enum BulletDragTypes public float bulletDmgMult = 1; public string explModelPath; public string explSoundPath; - public bool explosive = false; //general params public bool incendiary; @@ -110,7 +113,7 @@ public enum BulletDragTypes public float timeToLiveUntil; Light lightFlash; bool wasInitiated; - public Vector3 currentVelocity; + public Vector3 currentVelocity; // Current real velocity w/o offloading public float bulletMass; public float caliber = 1; public float bulletVelocity; //muzzle velocity @@ -147,6 +150,7 @@ public enum BulletDragTypes static RaycastHit[] hits; static RaycastHit[] reverseHits; static Collider[] overlapSphereColliders; + static Collider[] proximityOverlapSphereColliders; static List allHits; static Dictionary rayLength; private Vector3[] linePositions = new Vector3[2]; @@ -154,6 +158,10 @@ public enum BulletDragTypes private List partsHit = new List(); private double distanceTraveled = 0; + private double distanceLastHit = double.PositiveInfinity; + private double initialHitDistance = 0; + private float kDist = 1; + public double DistanceTraveled { get { return distanceTraveled; } } void Awake() @@ -161,6 +169,7 @@ void Awake() if (hits == null) { hits = new RaycastHit[100]; } if (reverseHits == null) { reverseHits = new RaycastHit[100]; } if (overlapSphereColliders == null) { overlapSphereColliders = new Collider[1000]; } + if (proximityOverlapSphereColliders == null) { proximityOverlapSphereColliders = new Collider[100]; } if (allHits == null) { allHits = new List(); } if (rayLength == null) { rayLength = new Dictionary(); } } @@ -170,7 +179,7 @@ void OnEnable() startPosition = transform.position; currentSpeed = currentVelocity.magnitude; // this is the velocity used for drag estimations (only), use total velocity, not muzzle velocity - if (explosive) + if (HEType != PooledBulletTypes.Slug) { HERatio = Mathf.Clamp(tntMass / (bulletMass < tntMass ? tntMass * 1.25f : bulletMass), 0.01f, 0.95f); } @@ -201,6 +210,10 @@ void OnEnable() } } distanceTraveled = 0; // Reset the distance travelled for the bullet (since it comes from a pool). + distanceLastHit = double.PositiveInfinity; // Reset variables used in post-penetration calculations. + initialHitDistance = 0; + kDist = 1; + dragVelocityFactor = 1; if (!wasInitiated) { @@ -238,11 +251,7 @@ void OnEnable() { bulletTrail.positionCount = linePositions.Length; } - linePositions[0] = transform.position + ((currentVelocity - FlightGlobals.ActiveVessel.Velocity()) * tracerDeltaFactor * 0.45f * Time.fixedDeltaTime); - // linePositions[0] = transform.position + currentVelocity * tracerDeltaFactor * 0.45f * Time.fixedDeltaTime; - // linePositions[0] = transform.position + (currentVelocity + 0.5f * Time.fixedDeltaTime * FlightGlobals.getGeeForceAtPosition(transform.position)) * Time.fixedDeltaTime; // DEBUG Show the bullet path over the next fixedDeltaTime. - linePositions[1] = transform.position; - bulletTrail.SetPositions(linePositions); + // Note: call SetTracerPosition() after enabling the bullet and making adjustments to it's position. if (!shaderInitialized) { @@ -273,13 +282,12 @@ void OnEnable() if (this.sourceVessel) { sourceVesselName = sourceVessel.GetName(); // Set the source vessel name as the vessel might have changed its name or died by the time the bullet hits. - BDACompetitionMode.Instance.Scores.RegisterShot(sourceVesselName); } else { sourceVesselName = null; } - if (caliber > 60) + if (caliber >= BDArmorySettings.APS_THRESHOLD) //if (caliber > 60) { BDATargetManager.FiredBullets.Add(this); } @@ -292,7 +300,7 @@ void OnDisable() CurrentPart = null; sabot = false; partsHit.Clear(); - if (caliber > 60) + if (caliber >= BDArmorySettings.APS_THRESHOLD) //if (caliber > 60) { BDATargetManager.FiredBullets.Remove(this); } @@ -337,22 +345,10 @@ void FixedUpdate() } //floating origin and velocity offloading corrections - if (!FloatingOrigin.Offset.IsZero() || !Krakensbane.GetFrameVelocity().IsZero()) - { - transform.position -= FloatingOrigin.OffsetNonKrakensbane; - startPosition -= FloatingOrigin.OffsetNonKrakensbane; - } - - if (tracerLength == 0) + if (BDKrakensbane.IsActive) { - // visual tracer velocity is relative to the observer - linePositions[0] = transform.position + ((currentVelocity - FlightGlobals.ActiveVessel.Velocity()) * tracerDeltaFactor * 0.45f * Time.fixedDeltaTime); - // linePositions[0] = transform.position + currentVelocity * tracerDeltaFactor * 0.45f * Time.fixedDeltaTime; - // linePositions[0] = transform.position + (currentVelocity + 0.5f * Time.fixedDeltaTime * FlightGlobals.getGeeForceAtPosition(transform.position)) * Time.fixedDeltaTime; // DEBUG Show the bullet path over the next fixedDeltaTime. - } - else - { - linePositions[0] = transform.position + ((currentVelocity - FlightGlobals.ActiveVessel.Velocity()).normalized * tracerLength); + transform.position -= BDKrakensbane.FloatingOriginOffsetNonKrakensbane; + startPosition -= BDKrakensbane.FloatingOriginOffsetNonKrakensbane; } if (fadeColor) @@ -360,13 +356,16 @@ void FixedUpdate() FadeColor(); bulletTrail.material.SetColor("_TintColor", currentColor * tracerLuminance); } - linePositions[1] = transform.position; - - bulletTrail.SetPositions(linePositions); + SetTracerPosition(); if (Time.time > timeToLiveUntil) //kill bullet when TTL ends { KillBullet(); + if (isAPSprojectile) + { + if (HEType != PooledBulletTypes.Explosive && tntMass > 0) + ExplosionFx.CreateExplosion(currPosition, tntMass, explModelPath, explSoundPath, ExplosionSourceType.Bullet, caliber, null, sourceVesselName, null, default, -1, true); + } return; } /* @@ -395,12 +394,10 @@ void FixedUpdate() { if (caliber < 75f) { - if (explosive) + if (HEType != PooledBulletTypes.Slug) ExplosionFx.CreateExplosion(currPosition, tntMass, explModelPath, explSoundPath, ExplosionSourceType.Bullet, caliber, null, sourceVesselName, null, default, -1, false, bulletMass, -1, dmgMult); if (nuclear) - { - NukeFX.CreateExplosion(currPosition, ExplosionSourceType.BattleDamage, sourceVesselName, bullet.DisplayName, 0, tntMass * 200, tntMass, tntMass, EMP, blastSoundPath, flashModelPath, shockModelPath, blastModelPath, plumeModelPath, debrisModelPath, "", ""); - } + NukeFX.CreateExplosion(currPosition, ExplosionSourceType.Bullet, sourceVesselName, bullet.DisplayName, 0, tntMass * 200, tntMass, tntMass, EMP, blastSoundPath, flashModelPath, shockModelPath, blastModelPath, plumeModelPath, debrisModelPath, "", ""); hasDetonated = true; KillBullet(); @@ -408,7 +405,7 @@ void FixedUpdate() } else { - if (explosive) + if (HEType != PooledBulletTypes.Slug) { if (fuzeType == BulletFuzeTypes.Delay || fuzeType == BulletFuzeTypes.Penetrating) { @@ -417,10 +414,10 @@ void FixedUpdate() } else //if (fuzeType != BulletFuzeTypes.None) { - if (explosive) + if (HEType != PooledBulletTypes.Slug) ExplosionFx.CreateExplosion(currPosition, tntMass, explModelPath, explSoundPath, ExplosionSourceType.Bullet, caliber, null, sourceVesselName, null, default, -1, false, bulletMass, -1, dmgMult); if (nuclear) - NukeFX.CreateExplosion(currPosition, ExplosionSourceType.BattleDamage, sourceVesselName, bullet.DisplayName, 0, tntMass * 200, tntMass, tntMass, EMP, blastSoundPath, flashModelPath, shockModelPath, blastModelPath, plumeModelPath, debrisModelPath, "", ""); + NukeFX.CreateExplosion(currPosition, ExplosionSourceType.Bullet, sourceVesselName, bullet.DisplayName, 0, tntMass * 200, tntMass, tntMass, EMP, blastSoundPath, flashModelPath, shockModelPath, blastModelPath, plumeModelPath, debrisModelPath, "", ""); hasDetonated = true; FXMonger.Splash(transform.position, caliber / 2); KillBullet(); @@ -437,10 +434,10 @@ void FixedUpdate() if (ProximityAirDetonation((float)distanceTraveled)) { //detonate - if (explosive) - ExplosionFx.CreateExplosion(currPosition, tntMass, explModelPath, explSoundPath, ExplosionSourceType.Bullet, caliber, null, sourceVesselName, null, currentVelocity, -1, false, bulletMass, -1, dmgMult); + if (HEType != PooledBulletTypes.Slug) + ExplosionFx.CreateExplosion(currPosition, tntMass, explModelPath, explSoundPath, ExplosionSourceType.Bullet, caliber, null, sourceVesselName, null, HEType == PooledBulletTypes.Explosive ? default : currentVelocity, -1, false, bulletMass, -1, dmgMult, HEType == PooledBulletTypes.Shaped ? "shapedcharge" : "standard", null, HEType == PooledBulletTypes.Shaped ? apBulletMod : 1f, ProjectileUtils.isReportingWeapon(sourceWeapon) ? (float)DistanceTraveled : -1); if (nuclear) - NukeFX.CreateExplosion(currPosition, ExplosionSourceType.BattleDamage, sourceVesselName, bullet.DisplayName, 0, tntMass * 200, tntMass, tntMass, EMP, blastSoundPath, flashModelPath, shockModelPath, blastModelPath, plumeModelPath, debrisModelPath, "", ""); + NukeFX.CreateExplosion(currPosition, ExplosionSourceType.Bullet, sourceVesselName, bullet.DisplayName, 0, tntMass * 200, tntMass, tntMass, EMP, blastSoundPath, flashModelPath, shockModelPath, blastModelPath, plumeModelPath, debrisModelPath, "", ""); if (beehive) BeehiveDetonation(); hasDetonated = true; @@ -465,6 +462,7 @@ public void MoveBullet(float period) // Full-timestep position change (leapfrog integrator) transform.position += currentVelocity * period; //move bullet distanceTraveled += currentVelocity.magnitude * period; // calculate flight distance for achievement purposes + if (!underwater && FlightGlobals.getAltitudeAtPos(transform.position) <= 0) // Check if the bullet is now underwater. { float hitAngle = Vector3.Angle(GetDragAdjustedVelocity(), -VectorUtils.GetUpDirection(transform.position)); @@ -524,6 +522,7 @@ public bool CheckBulletCollisions(float period) hasPenetrated = true; hasDetonated = false; hasRicocheted = false; + CurrentPart = null; //penTicker = 0; currPosition = transform.position; allHits.Clear(); @@ -552,7 +551,7 @@ public void CheckBulletCollisionWithVessels(float period) if (!BDArmorySettings.VESSEL_RELATIVE_BULLET_CHECKS) return; List nearbyVessels = new List(); - var layerMask = (int)(LayerMasks.Parts | LayerMasks.EVA); + const int layerMask = (int)(LayerMasks.Parts | LayerMasks.EVA | LayerMasks.Wheels); var overlapSphereColliderCount = Physics.OverlapSphereNonAlloc(transform.position, currentVelocity.magnitude * period * 2f, overlapSphereColliders, layerMask); // Overlapsphere of 2*period assuming that vessels are moving at similar speeds to the bullet. if (overlapSphereColliderCount == overlapSphereColliders.Length) { @@ -590,7 +589,7 @@ public void CheckBulletCollisionWithVessel(float period, Vessel vessel) var relativeVelocity = currentVelocity - (Vector3)vessel.Velocity(); float dist = relativeVelocity.magnitude * period; bulletRay = new Ray(currPosition, relativeVelocity + 0.5f * period * FlightGlobals.getGeeForceAtPosition(transform.position)); - var layerMask = (int)(LayerMasks.Parts | LayerMasks.EVA); + const int layerMask = (int)(LayerMasks.Parts | LayerMasks.EVA | LayerMasks.Wheels); var hitCount = Physics.RaycastNonAlloc(bulletRay, hits, dist, layerMask); if (hitCount == hits.Length) // If there's a whole bunch of stuff in the way (unlikely), then we need to increase the size of our hits buffer. @@ -599,14 +598,18 @@ public void CheckBulletCollisionWithVessel(float period, Vessel vessel) hitCount = hits.Length; } - var reverseHitCount = Physics.RaycastNonAlloc(new Ray(currPosition + relativeVelocity * period, -relativeVelocity), reverseHits, dist, layerMask); + var reverseRay = new Ray(bulletRay.origin + dist * bulletRay.direction, -bulletRay.direction); + var reverseHitCount = Physics.RaycastNonAlloc(reverseRay, reverseHits, dist, layerMask); if (reverseHitCount == reverseHits.Length) { - reverseHits = Physics.RaycastAll(new Ray(currPosition + relativeVelocity * period, -relativeVelocity), dist, layerMask); + reverseHits = Physics.RaycastAll(reverseRay, dist, layerMask); reverseHitCount = reverseHits.Length; } for (int i = 0; i < reverseHitCount; ++i) - { reverseHits[i].distance = dist - reverseHits[i].distance; } + { + reverseHits[i].distance = dist - reverseHits[i].distance; + reverseHits[i].normal = -reverseHits[i].normal; + } if (hitCount + reverseHitCount > 0) { @@ -646,14 +649,18 @@ public void CheckBulletCollisionWithScenery(float period) } allHits.AddRange(hits.Take(hitCount)); - var reverseHitCount = Physics.RaycastNonAlloc(new Ray(currPosition + currentVelocity * period, -currentVelocity), reverseHits, dist, layerMask); + var reverseRay = new Ray(bulletRay.origin + dist * bulletRay.direction, -bulletRay.direction); + var reverseHitCount = Physics.RaycastNonAlloc(reverseRay, reverseHits, dist, layerMask); if (reverseHitCount == reverseHits.Length) { - reverseHits = Physics.RaycastAll(new Ray(currPosition + currentVelocity * period, -currentVelocity), dist, layerMask); + reverseHits = Physics.RaycastAll(reverseRay, dist, layerMask); reverseHitCount = reverseHits.Length; } for (int i = 0; i < reverseHitCount; ++i) - { reverseHits[i].distance = dist - reverseHits[i].distance; } + { + reverseHits[i].distance = dist - reverseHits[i].distance; + reverseHits[i].normal = -reverseHits[i].normal; + } allHits.AddRange(reverseHits.Take(reverseHitCount)); } @@ -667,7 +674,7 @@ public bool CheckBulletCollision(float period) { float dist = currentVelocity.magnitude * period; bulletRay = new Ray(currPosition, currentVelocity + 0.5f * period * FlightGlobals.getGeeForceAtPosition(transform.position)); - var layerMask = (int)(LayerMasks.Parts | LayerMasks.EVA | LayerMasks.Scenery); + const int layerMask = (int)(LayerMasks.Parts | LayerMasks.EVA | LayerMasks.Wheels | LayerMasks.Scenery); var hitCount = Physics.RaycastNonAlloc(bulletRay, hits, dist, layerMask); if (hitCount == hits.Length) // If there's a whole bunch of stuff in the way (unlikely), then we need to increase the size of our hits buffer. { @@ -675,17 +682,22 @@ public bool CheckBulletCollision(float period) hitCount = hits.Length; } - var reverseHitCount = Physics.RaycastNonAlloc(new Ray(currPosition + currentVelocity * period, -currentVelocity), reverseHits, dist, layerMask); + var reverseRay = new Ray(bulletRay.origin + dist * bulletRay.direction, -bulletRay.direction); + var reverseHitCount = Physics.RaycastNonAlloc(reverseRay, reverseHits, dist, layerMask); if (reverseHitCount == reverseHits.Length) { - reverseHits = Physics.RaycastAll(new Ray(currPosition + currentVelocity * period, -currentVelocity), dist, layerMask); + reverseHits = Physics.RaycastAll(reverseRay, dist, layerMask); reverseHitCount = reverseHits.Length; } for (int i = 0; i < reverseHitCount; ++i) - { reverseHits[i].distance = dist - reverseHits[i].distance; } + { + reverseHits[i].distance = dist - reverseHits[i].distance; + reverseHits[i].normal = -reverseHits[i].normal; + } if (hitCount + reverseHitCount > 0) { + // Note: this should probably use something like the CollateHits function in ExplosionFX, but doesn't seem to be as performance critical here. var orderedHits = hits.Take(hitCount).Concat(reverseHits.Take(reverseHitCount)).OrderBy(x => x.distance); using (var hit = orderedHits.GetEnumerator()) while (hit.MoveNext()) if (BulletHitAnalysis(hit.Current, period)) return true; @@ -723,14 +735,15 @@ bool BulletHitAnalysis(RaycastHit hit, float period) if (hitPart != null && ProjectileUtils.IsIgnoredPart(hitPart)) return false; // Ignore ignored parts. if (hitPart != null && hitPart == sourceWeapon) return false; // Ignore weapon that fired the bullet. - if (hitPart != null && (hitPart == CurrentPart && CurrentPart.name.ToLower().Contains("armor"))) return false; //only have bullet hit armor panels once - no back armor to hit if penetration + if (hitPart != null && (hitPart == CurrentPart && ProjectileUtils.IsArmorPart(CurrentPart))) return false; //only have bullet hit armor panels once - no back armor to hit if penetration + CurrentPart = hitPart; if (hitEVA != null) { hitPart = hitEVA.part; // relative velocity, separate from the below statement, because the hitpart might be assigned only above if (hitPart.rb != null) - impactSpeed = (GetDragAdjustedVelocity() - (hitPart.rb.velocity + Krakensbane.GetFrameVelocityV3f())).magnitude; + impactSpeed = (GetDragAdjustedVelocity() - (hitPart.rb.velocity + BDKrakensbane.FrameVelocityV3f)).magnitude; else impactSpeed = GetDragAdjustedVelocity().magnitude; distanceTraveled += hit.distance; @@ -740,10 +753,10 @@ bool BulletHitAnalysis(RaycastHit hit, float period) } else { - ProjectileUtils.ApplyDamage(hitPart, hit, dmgMult, 1, caliber, bulletMass, impactSpeed, bulletDmgMult, distanceTraveled, explosive, incendiary, hasRicocheted, sourceVessel, bullet.name, team, ExplosionSourceType.Bullet, true, true, true); + ProjectileUtils.ApplyDamage(hitPart, hit, dmgMult, 1, caliber, bulletMass, impactSpeed, bulletDmgMult, distanceTraveled, HEType != PooledBulletTypes.Slug ? true : false, incendiary, hasRicocheted, sourceVessel, bullet.DisplayName, team, ExplosionSourceType.Bullet, true, true, true); } ExplosiveDetonation(hitPart, hit, bulletRay); - ProjectileUtils.StealResources(hitPart, sourceVessel, stealResources); + ResourceUtils.StealResources(hitPart, sourceVessel, stealResources); KillBullet(); // Kerbals are too thick-headed for penetration... return true; } @@ -755,7 +768,80 @@ bool BulletHitAnalysis(RaycastHit hit, float period) { // using relative velocity vector instead of just bullet velocity // since KSP vessels might move faster than bullets - impactVelocity -= (hitPart.rb.velocity + Krakensbane.GetFrameVelocityV3f()); + impactVelocity -= (hitPart.rb.velocity + BDKrakensbane.FrameVelocityV3f); + } + + impactSpeed = impactVelocity.magnitude; + + float length = ((bulletMass * 1000.0f * 400.0f) / ((caliber * caliber * + Mathf.PI) * (sabot ? 19.0f : 11.34f)) + 1.0f) * 10.0f; + + // New system to wear down hypervelocity projectiles over distance + // This is based on an equation that was derived for shaped charges. Now this isn't + // exactly accurate in our case, or rather it's not accurate at all, but it's something + // in the ballpark and it's also more-or-less going to give the proper behavior for + // spaced armor at high velocities. This is sourced from https://www.diva-portal.org/smash/get/diva2:643824/FULLTEXT01.pdf + // and once again, I must emphasize. This is for shaped charges, it's not for post-penetration + // behavior of hypervelocity projectiles, but I'm going to hand-wave away the difference between + // the plasma that flies out after a penetration and the armor-piercing jet of a shaped charge. + if (impactSpeed > 2500 || !double.IsPositiveInfinity(distanceLastHit)) + { + // This only applies to anything that will actually survive an impact so EMP, impulse and any HE rounds that explode right away are out + if (!EMP || impulse != 0 || ((HERatio > 0) && (fuzeType != BulletFuzeTypes.Penetrating || fuzeType != BulletFuzeTypes.Delay))) + { + + // This is just because if distanceSinceHit < (7f * caliber * 10f) penetration will be worse, this behavior is true of + // shaped charges due to the jet formation distance, however we're going to ignore it since it isn't true of a hypervelocity + // projectile that's just smashed through some armor. + //if ((distanceTraveled + hit.distance - distanceLastHit) * 1000f > (7f * caliber * 10f)) + + // Changed from the previous 7 * caliber * 10 maximum to just > caliber since that no longer exists. + if ((distanceTraveled + hit.distance - distanceLastHit) * 1000f > caliber) + { + // The formula is based on distance and the caliber of the shaped charge, now since in our case we are talking + // about projectiles rather than shaped charges we'll take the projectile diameter and call that the jet size. + // Shaped charge jets have a diameter generally around 5% of the shaped charge's caliber, however in our case + // this means these projectiles wouldn't bleed off that hard with distance, thus we'll say they're 10% of the + // shaped charge's caliber. + // Calculating this term once since division is expensive + //float kTerm = ((float)(distanceTraveled + hit.distance - distanceLastHit) * 1000f - 7f * 10f *caliber) / (14f * 10f * caliber); + // Modified this from the original formula, commented out above to remove the standoff distance required for jet formation. + // Just makes more sense to me not to have it in there. + float kTerm = ((float)(distanceTraveled + hit.distance - distanceLastHit) * 1000f) / (14f * 10f * caliber); + + kDist = 1f / (kDist * (1f + kTerm * kTerm)); // Then using it in the formula + + // If the projectile gets too small things go wonky with the formulas for penetration + // they'll still work honestly, but I'd rather avoid those situations + /*if ((kDist * length) < 1.2f * caliber) + { + float massFactor = (1.2f * caliber / length); + bulletMass = bulletMass * massFactor; + length = (length - 10) * massFactor + 10; + } + else + { + bulletMass = bulletMass * kDist; + length = (length - 10) * kDist + 10; + }*/ + + // Deprecated above since the penetration formula was modified to + // deal with such cases + bulletMass = bulletMass * kDist; + length = (length - 10) * kDist + 10; + + if (BDArmorySettings.DEBUG_WEAPONS) + { + Debug.Log("[BDArmory.PooledBullet] kDist: " + kDist + ". Distance Since Last Hit: " + (distanceTraveled + hit.distance - distanceLastHit) + " m."); + } + } + + if (double.IsPositiveInfinity(distanceLastHit)) + { + // Add the distance since this hit so the next part that gets hit has the proper distance + distanceLastHit = distanceTraveled + hit.distance; + } + } } float hitAngle = Vector3.Angle(impactVelocity, -hit.normal); @@ -763,7 +849,7 @@ bool BulletHitAnalysis(RaycastHit hit, float period) if (ProjectileUtils.CheckGroundHit(hitPart, hit, caliber)) { - ProjectileUtils.CheckBuildingHit(hit, bulletMass, currentVelocity, bulletDmgMult); + if (!BDArmorySettings.PAINTBALL_MODE) ProjectileUtils.CheckBuildingHit(hit, bulletMass, impactVelocity, bulletDmgMult); if (!RicochetScenery(hitAngle)) { ExplosiveDetonation(hitPart, hit, bulletRay); @@ -784,7 +870,8 @@ bool BulletHitAnalysis(RaycastHit hit, float period) if (hitPart == null) return false; // Hits below here are part hits. //Standard Pipeline Hitpoints, Armor and Explosives - impactSpeed = impactVelocity.magnitude; + //impactSpeed = impactVelocity.magnitude; //Moved up for the projectile weardown calculation + if (initialHitDistance == 0) initialHitDistance = distanceTraveled + hit.distance; if (massMod != 0) { var ME = hitPart.FindModuleImplementing(); @@ -809,8 +896,8 @@ bool BulletHitAnalysis(RaycastHit hit, float period) { distanceTraveled += hit.distance; if (!BDArmorySettings.PAINTBALL_MODE) - { hitPart.rb.AddForceAtPosition(impactVelocity.normalized * impulse, hit.point, ForceMode.Acceleration); } - ProjectileUtils.ApplyScore(hitPart, sourceVessel.GetName(), distanceTraveled, 0, bullet.name, ExplosionSourceType.Bullet, true); + { hitPart.rb.AddForceAtPosition(impactVelocity.normalized * impulse, hit.point, ForceMode.Impulse); } + ProjectileUtils.ApplyScore(hitPart, sourceVessel.GetName(), distanceTraveled, 0, bullet.DisplayName, ExplosionSourceType.Bullet, true); if (BDArmorySettings.BULLET_HITS) { BulletHitFX.CreateBulletHit(hitPart, hit.point, hit, hit.normal, false, caliber, 0, team); @@ -824,6 +911,7 @@ bool BulletHitAnalysis(RaycastHit hit, float period) //calculate armor strength float penetration = 0; float penetrationFactor = 0; + //float length = 0; //Moved up for the new bullet wear over distance system var Armor = hitPart.FindModuleImplementing(); if (Armor != null) { @@ -832,20 +920,83 @@ bool BulletHitAnalysis(RaycastHit hit, float period) float Strength = Armor.Strength; float safeTemp = Armor.SafeUseTemp; float Density = Armor.Density; + + float vFactor = Armor.vFactor; + float muParam1; + float muParam2; + float muParam3; + + if (hitPart.skinTemperature > safeTemp) //has the armor started melting/denaturing/whatever? + { + //vFactor *= 1/(1.25f*0.75f-0.25f*0.75f*0.75f); + vFactor *= 1.25490196078f; // Uses the above equation but just calculated out. + // The equation 1/(1.25*x-0.25*x^2) approximates the effect of changing yield strength + // by a factor of x + if (hitPart.skinTemperature > safeTemp * 1.5f) + { + vFactor *= 1.77777777778f; // Same as used above, but here with x = 0.5. Maybe this should be + // some kind of a curve? + } + } + int armorType = (int)Armor.ArmorTypeNum; - if (BDArmorySettings.DRAW_ARMOR_LABELS) + if (BDArmorySettings.DEBUG_ARMOR) { - Debug.Log("[PooledBullet].ArmorVars found: Strength : " + Strength + "; Ductility: " + Ductility + "; Hardness: " + hardness + "; MaxTemp: " + safeTemp + "; Density: " + Density + "; thickness: " + thickness); + Debug.Log($"[BDArmory.PooledBullet]: ArmorVars found: Strength : {Strength}; Ductility: {Ductility}; Hardness: {hardness}; MaxTemp: {safeTemp}; Density: {Density}; thickness: {thickness}; hit angle: {hitAngle}"); } - float bulletEnergy = ProjectileUtils.CalculateProjectileEnergy(bulletMass, impactSpeed); - float armorStrength = ProjectileUtils.CalculateArmorStrength(caliber, thickness, Ductility, Strength, Density, safeTemp, hitPart); + //calculate bullet deformation float newCaliber = caliber; + //length = ((bulletMass * 1000.0f * 400.0f) / ((caliber * caliber * + // Mathf.PI) * (sabot ? 19.0f : 11.34f)) + 1.0f) * 10.0f; //Moved up for the purposes of the new bullet wear over distance system + + /* + if (Ductility > 0.05) + { + */ + if (!sabot) { - newCaliber = ProjectileUtils.CalculateDeformation(armorStrength, bulletEnergy, caliber, impactSpeed, hardness, Density, HERatio, apBulletMod); + // Moved the bulletEnergy and armorStrength calculations here because + // they are no longer needed for CalculatePenetration. This should + // improve performance somewhat for sabot rounds, which is a good + // thing since that new model requires the use of Mathf.Log and + // Mathf.Exp. + float bulletEnergy = ProjectileUtils.CalculateProjectileEnergy(bulletMass, impactSpeed); + float armorStrength = ProjectileUtils.CalculateArmorStrength(caliber, thickness, Ductility, Strength, Density, safeTemp, hitPart); + newCaliber = ProjectileUtils.CalculateDeformation(armorStrength, bulletEnergy, caliber, impactSpeed, hardness, Density, HERatio, apBulletMod, sabot); + + // Also set the params to the non-sabot ones + muParam1 = Armor.muParam1; + muParam2 = Armor.muParam2; + muParam3 = Armor.muParam3; + } + else + { + // If it's a sabot just set the params to the sabot ones + muParam1 = Armor.muParam1S; + muParam2 = Armor.muParam2S; + muParam3 = Armor.muParam3S; + } + //penetration = ProjectileUtils.CalculatePenetration(caliber, newCaliber, bulletMass, impactSpeed, Ductility, Density, Strength, thickness, apBulletMod, sabot); + penetration = ProjectileUtils.CalculatePenetration(caliber, impactSpeed, bulletMass, apBulletMod, Strength, vFactor, muParam1, muParam2, muParam3, sabot, length); + + if (BDArmorySettings.DEBUG_WEAPONS) + { + Debug.Log("[BDArmory.PooledBullet] Penetration: " + penetration + "mm. impactSpeed: " + impactSpeed + "m/s. bulletMass = " + bulletMass + "kg. Caliber: " + caliber + "mm. Length: " + length + "mm. Sabot: " + sabot); + } + + /* + } + else + { + float bulletEnergy = ProjectileUtils.CalculateProjectileEnergy(bulletMass, impactSpeed); + float armorStrength = ProjectileUtils.CalculateArmorStrength(caliber, thickness, Ductility, Strength, Density, safeTemp, hitPart); + newCaliber = ProjectileUtils.CalculateDeformation(armorStrength, bulletEnergy, caliber, impactSpeed, hardness, Density, HERatio, apBulletMod, sabot); + penetration = ProjectileUtils.CalculateCeramicPenetration(caliber, newCaliber, bulletMass, impactSpeed, Ductility, Density, Strength, thickness, apBulletMod, sabot); } - penetration = ProjectileUtils.CalculatePenetration(caliber, newCaliber, bulletMass, impactSpeed, Ductility, Density, Strength, thickness, apBulletMod, sabot); + */ + caliber = newCaliber; //update bullet with new caliber post-deformation(if any) penetrationFactor = ProjectileUtils.CalculateArmorPenetration(hitPart, penetration, thickness); //Reactive Armor calcs @@ -858,7 +1009,7 @@ bool BulletHitAnalysis(RaycastHit hit, float period) if (penetrationFactor > 1) { float thicknessModifier = RA.armorModifier; - if (BDArmorySettings.DRAW_ARMOR_LABELS) Debug.Log("[PooledBullet] Beginning Reactive Armor Hit; NXRA: " + RA.NXRA + "; thickness Mod: " + RA.armorModifier); + if (BDArmorySettings.DEBUG_ARMOR) Debug.Log("[BDArmory.PooledBullet]: Beginning Reactive Armor Hit; NXRA: " + RA.NXRA + "; thickness Mod: " + RA.armorModifier); if (RA.NXRA) //non-explosive RA, always active { thickness *= thicknessModifier; @@ -869,10 +1020,10 @@ bool BulletHitAnalysis(RaycastHit hit, float period) { if (hitAngle < 80) //ERA isn't going to do much against near-perpendicular hits { - caliber = Mathf.Sqrt((caliber * (((bulletMass * 1000) / ((caliber * caliber * Mathf.PI / 400) * 19)) + 1) * 4) / Mathf.PI); //increase caliber to sim sabot hitting perpendicualr instead of point-first + caliber = BDAMath.Sqrt((caliber * (((bulletMass * 1000) / ((caliber * caliber * Mathf.PI / 400) * 19)) + 1) * 4) / Mathf.PI); //increase caliber to sim sabot hitting perpendicualr instead of point-first bulletMass /= 2; //sunder sabot //RA isn't going to stop sabot, but underlying part's armor will (probably) - if (BDArmorySettings.DRAW_ARMOR_LABELS) Debug.Log("[PooledBullet] Sabot caliber and mass now: " + caliber + ", " + bulletMass); + if (BDArmorySettings.DEBUG_ARMOR) Debug.Log("[BDArmory.PooledBullet]: Sabot caliber and mass now: " + caliber + ", " + bulletMass); RA.UpdateSectionScales(); } } @@ -920,6 +1071,7 @@ bool BulletHitAnalysis(RaycastHit hit, float period) if (fuzeType == BulletFuzeTypes.Impact || fuzeType == BulletFuzeTypes.Timed) { ExplosiveDetonation(hitPart, hit, bulletRay); + ProjectileUtils.CalculateShrapnelDamage(hitPart, hit, caliber, tntMass, 0, sourceVesselName, ExplosionSourceType.Bullet, bulletMass, penetrationFactor); //calc daamge from bullet exploding } if (fuzeType == BulletFuzeTypes.Delay) { @@ -938,7 +1090,7 @@ bool BulletHitAnalysis(RaycastHit hit, float period) hitPart.rb.AddForceAtPosition(impactVelocity.normalized * accelerationMagnitude, hit.point, ForceMode.Acceleration); - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_WEAPONS) Debug.Log("[BDArmory.PooledBullet]: Force Applied " + Math.Round(accelerationMagnitude, 2) + "| Vessel mass in kgs=" + hitPart.vessel.GetTotalMass() * 1000 + "| bullet effective mass =" + (bulletMass - tntMass)); } distanceTraveled += hit.distance; @@ -954,7 +1106,8 @@ bool BulletHitAnalysis(RaycastHit hit, float period) StopCoroutine(DelayedDetonationRoutine()); } ExplosiveDetonation(hitPart, hit, bulletRay); - ProjectileUtils.ApplyScore(hitPart, sourceVesselName, distanceTraveled, 0, bullet.name, ExplosionSourceType.Bullet, penTicker > 0 ? false : true); + ProjectileUtils.CalculateShrapnelDamage(hitPart, hit, caliber, tntMass, 0, sourceVesselName, ExplosionSourceType.Bullet, bulletMass, penetrationFactor); //calc damage from bullet exploding + ProjectileUtils.ApplyScore(hitPart, sourceVesselName, distanceTraveled, 0, bullet.DisplayName, ExplosionSourceType.Bullet, penTicker > 0 ? false : true); hasDetonated = true; KillBullet(); return true; @@ -962,8 +1115,114 @@ bool BulletHitAnalysis(RaycastHit hit, float period) } else //penetration >= 1 { - currentVelocity = currentVelocity * (1 - (float)Math.Sqrt(thickness / penetration)); - impactVelocity = impactVelocity * (1 - (float)Math.Sqrt(thickness / penetration)); + // Old Post Pen Behavior + //currentVelocity = currentVelocity * (1 - (float)Math.Sqrt(thickness / penetration)); + //impactVelocity = impactVelocity * (1 - (float)Math.Sqrt(thickness / penetration)); + + // New Post Pen Behavior, this is quite game-ified and not really based heavily on + // actual proper equations, however it does try to get the same kind of behavior as + // would be found IRL. Primarily, this means high velocity impacts will be mostly + // eroding the projectile rather than slowing it down (all studies of this behavior + // show residual velocity only decreases slightly during penetration while the + // projectile is getting eroded, then starts decreasing rapidly as the projectile + // approaches a L/D ratio of 1. Erosion is drastically increased at 2500 m/s + in + // order to try and replicate the projectile basically vaporizing at high velocities. + // Note that the velocity thresholds are really mostly arbitrary and that the vaporizing + // behavior isn't that accurate since the projectile would remain semi-coherent immediately + // after penetration and would disperse over time, hence the spacing in stuff like + // whipple shields but that behavior is fairly complex and I'm already in way over my head. + + // Calculating this ratio once since we're going to need it a bunch + float adjustedPenRatio = (1 - BDAMath.Sqrt(thickness / penetration)); + + // If impact is at high speed + if (impactSpeed > 1200f) + { + // If the projectile is still above a L/D ratio of 1.1 (should be 1 but I want to + // avoid the edge case in the pen formula where L/D = 1) + if (length / caliber > 1.1f) + { + // Then we set the mass ratio to the default for impacts under 2500 m/s + // we take off 5% by default to decrease penetration efficiency through + // multiple plates a little more + float massRatio = 0.975f * adjustedPenRatio; + + if (impactSpeed > 2500f) + { + // If impact speed is really high then spaced armor wil work exceptionally + // well, with increasing results as the velocity of the projectile goes + // higher. This is mostly to make whipple shields viable and desireable + // in railgun combat. Ideally we'd be modelling the projectile vaporizing + // and then losing coherence as it travels through empty space between the + // outer plate and the inner plate but I'm not quite sure how that behavior + // would look like. Best way to probably do that is to decrease projectile + // lifespan and to add a lastImpact timestamp and do some kind of decrease + // in mass as a function of the time between impacts. + //massRatio = 2375f / impactSpeed * adjustedPenRatio; + + // Adjusted the above formula so only up to 50% of the mass could be lost + // immediately upon penetration (to being vaporized). At this point this + // stuff I've accepted is going to be purely gameified. If anybody has + // an expertise in hypervelocity impact mechanics they're welcome to + // change all this stuff I'm doing for hypervelocity stuff. + massRatio = (0.45f + 0.5f * (2500f / impactSpeed)) * adjustedPenRatio; + } + + + + // We cap the minimum L/D to be 1.2 to avoid that edge case in the pen formula + if ((massRatio * (length - 10f) + 10f) < (1.1f * caliber)) + { + float ratio; + + if (caliber < 10f || length < 10.05f) + { + ratio = 1.1f * caliber / length; + } + else + { + ratio = (1.1f * caliber - 10f) / (length - 10f); + } + + if (ratio > 1) + { + Debug.LogError($"DEBUG Bullet Ratio: {ratio} is greater than 1! Length: {length} Caliber: {caliber}."); + ratio = 1; + } + + bulletMass *= ratio; + + adjustedPenRatio /= ratio; + + // In the case we are reaching that cap we decrease the velocity by + // the adjustedPenRatio minus the portion that went into erosion + currentVelocity = currentVelocity * adjustedPenRatio; + impactVelocity = impactVelocity * adjustedPenRatio; + } + else + { + bulletMass = bulletMass * massRatio; + + // If we don't, I.E. the round isn't completely eroded, we decrease + // the velocity by a max of 5%, proportional to the adjustedPenRatio + currentVelocity = currentVelocity * (0.95f + 0.05f * adjustedPenRatio); + impactVelocity = impactVelocity * (0.95f + 0.05f * adjustedPenRatio); + } + } + else + { + // If the projectile has already been eroded away we just decrease the + // velocity by the adjustedPenRatio + currentVelocity = currentVelocity * adjustedPenRatio; + impactVelocity = impactVelocity * adjustedPenRatio; + } + } + else + { + // Low velocity impacts behave the same as before + currentVelocity = currentVelocity * adjustedPenRatio; + impactVelocity = impactVelocity * adjustedPenRatio; + } currentSpeed = currentVelocity.magnitude; timeElapsedSinceCurrentSpeedWasAdjusted = 0; @@ -976,25 +1235,25 @@ bool BulletHitAnalysis(RaycastHit hit, float period) hasPenetrated = true; bool viableBullet = ProjectileUtils.CalculateBulletStatus(bulletMass, caliber, sabot); - ProjectileUtils.StealResources(hitPart, sourceVessel, stealResources); + ResourceUtils.StealResources(hitPart, sourceVessel, stealResources); //ProjectileUtils.CheckPartForExplosion(hitPart); if (dmgMult < 0) { hitPart.AddInstagibDamage(); - ProjectileUtils.ApplyScore(hitPart, sourceVessel.GetName(), distanceTraveled, 0, bullet.name, ExplosionSourceType.Bullet, true); + ProjectileUtils.ApplyScore(hitPart, sourceVessel.GetName(), distanceTraveled, 0, bullet.DisplayName, ExplosionSourceType.Bullet, true); } else { - float cockpitPen = (float)(16f * impactVelocity.magnitude * Mathf.Sqrt(bulletMass / 1000) / Mathf.Sqrt(caliber) * apBulletMod); //assuming a 20mm steel armor plate for cockpit armor - ProjectileUtils.ApplyDamage(hitPart, hit, dmgMult, penetrationFactor, caliber, bulletMass, currentVelocity.magnitude, viableBullet ? bulletDmgMult : bulletDmgMult / 2, distanceTraveled, explosive, incendiary, hasRicocheted, sourceVessel, bullet.name, team, ExplosionSourceType.Bullet, penTicker > 0 ? false : true, partsHit.Contains(hitPart) ? false : true, (cockpitPen > Mathf.Max(20 / anglemultiplier, 1)) ? true : false); + float cockpitPen = (float)(16f * impactVelocity.magnitude * BDAMath.Sqrt(bulletMass / 1000) / BDAMath.Sqrt(caliber) * apBulletMod); //assuming a 20mm steel armor plate for cockpit armor + ProjectileUtils.ApplyDamage(hitPart, hit, dmgMult, penetrationFactor, caliber, bulletMass, currentVelocity.magnitude, viableBullet ? bulletDmgMult : bulletDmgMult / 2, distanceTraveled, HEType != PooledBulletTypes.Slug ? true : false, incendiary, hasRicocheted, sourceVessel, bullet.name, team, ExplosionSourceType.Bullet, penTicker > 0 ? false : true, partsHit.Contains(hitPart) ? false : true, (cockpitPen > Mathf.Max(20 / anglemultiplier, 1)) ? true : false); //need to add a check for if the bullet has already struck the part, since it doesn't make sense for some battledamage to apply on the second hit from the bullet exiting the part - wings/ctrl srfs, pilot kills, subsystem damage } //Delay and Penetrating Fuze bullets that penetrate should explode shortly after //if penetration is very great, they will have moved on //if (explosive && penetrationFactor < 3 || !viableBullet) - if (explosive) + if (HEType != PooledBulletTypes.Slug) { if (fuzeType == BulletFuzeTypes.Delay) { @@ -1003,7 +1262,7 @@ bool BulletHitAnalysis(RaycastHit hit, float period) //distanceTraveled += hit.distance; if (!fuzeTriggered) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.PooledBullet]: Delay Fuze Tripped"); + if (BDArmorySettings.DEBUG_WEAPONS) Debug.Log("[BDArmory.PooledBullet]: Delay Fuze Tripped at t: " + Time.time); fuzeTriggered = true; StartCoroutine(DelayedDetonationRoutine()); } @@ -1014,7 +1273,7 @@ bool BulletHitAnalysis(RaycastHit hit, float period) { if (!fuzeTriggered) { - //Debug.Log("[BDArmory.PooledBullet]: Delay Fuze Tripped"); + if (BDArmorySettings.DEBUG_WEAPONS) Debug.Log("[BDArmory.PooledBullet]: Penetrating Fuze Tripped at t: " + Time.time); fuzeTriggered = true; StartCoroutine(DelayedDetonationRoutine()); } @@ -1022,7 +1281,7 @@ bool BulletHitAnalysis(RaycastHit hit, float period) } else //impact by impact, Timed, Prox and Flak, if for whatever reason those last two have 0 proxi range { - //Debug.Log("[BDArmory.PooledBullet]: impact Fuze detonation"); + //if (BDArmorySettings.DEBUG_WEAPONS) Debug.Log("[BDArmory.PooledBullet]: impact Fuze detonation"); ExplosiveDetonation(hitPart, hit, bulletRay, true); ProjectileUtils.CalculateShrapnelDamage(hitPart, hit, caliber, tntMass, 0, sourceVesselName, ExplosionSourceType.Bullet, bulletMass, penetrationFactor); //calc daamge from bullet exploding hasDetonated = true; @@ -1032,7 +1291,7 @@ bool BulletHitAnalysis(RaycastHit hit, float period) } if (!viableBullet) { - //Debug.Log("[BDArmory.PooledBullet]: !viable bullet, removing"); + if (BDArmorySettings.DEBUG_WEAPONS) Debug.Log("[BDArmory.PooledBullet]: !viable bullet, removing"); ExplosiveDetonation(hitPart, hit, bulletRay, true); ProjectileUtils.CalculateShrapnelDamage(hitPart, hit, caliber, tntMass, 0, sourceVesselName, ExplosionSourceType.Bullet, bulletMass, penetrationFactor); //calc daamge from bullet exploding hasDetonated = true; @@ -1048,11 +1307,11 @@ bool BulletHitAnalysis(RaycastHit hit, float period) //smaller caliber rounds would be too deformed to do any further damage if (currentVelocity.magnitude <= 100 && hasPenetrated) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_WEAPONS) { Debug.Log("[BDArmory.PooledBullet]: Bullet Velocity too low, stopping"); } - KillBullet(); + if (!fuzeTriggered) KillBullet(); distanceTraveled += hit.distance; return true; } @@ -1066,10 +1325,10 @@ IEnumerator DelayedDetonationRoutine() fuzeTriggered = false; if (!hasDetonated) { - if (explosive) - ExplosionFx.CreateExplosion(currPosition, tntMass, explModelPath, explSoundPath, ExplosionSourceType.Bullet, caliber, null, sourceVesselName, null, default, -1, false, bulletMass, -1, dmgMult); + if (HEType != PooledBulletTypes.Slug) + ExplosionFx.CreateExplosion(currPosition, tntMass, explModelPath, explSoundPath, ExplosionSourceType.Bullet, caliber, null, sourceVesselName, null, HEType == PooledBulletTypes.Explosive ? default : currentVelocity, -1, false, bulletMass, -1, dmgMult, HEType == PooledBulletTypes.Shaped ? "shapedcharge" : "standard", CurrentPart, HEType == PooledBulletTypes.Shaped ? apBulletMod : 1f, ProjectileUtils.isReportingWeapon(sourceWeapon) ? (float)DistanceTraveled : -1); if (nuclear) - NukeFX.CreateExplosion(currPosition, ExplosionSourceType.BattleDamage, sourceVesselName, bullet.DisplayName, 0, tntMass * 200, tntMass, tntMass, EMP, blastSoundPath, flashModelPath, shockModelPath, blastModelPath, plumeModelPath, debrisModelPath, "", ""); + NukeFX.CreateExplosion(currPosition, ExplosionSourceType.Bullet, sourceVesselName, bullet.DisplayName, 0, tntMass * 200, tntMass, tntMass, EMP, blastSoundPath, flashModelPath, shockModelPath, blastModelPath, plumeModelPath, debrisModelPath, "", ""); hasDetonated = true; if (tntMass > 1) @@ -1081,6 +1340,7 @@ IEnumerator DelayedDetonationRoutine() FXMonger.Splash(FlightGlobals.currentMainBody.GetWorldSurfacePosition(latitudeAtPos, longitudeAtPos, 0), tntMass * 20); } } + if (BDArmorySettings.DEBUG_WEAPONS) Debug.Log("[BDArmory.PooledBullet]: Delayed Detonation at: " + Time.time); KillBullet(); } } @@ -1091,8 +1351,8 @@ public void BeehiveDetonation() Debug.Log("[BDArmory.PooledBullet] Beehive round not configured with subMunitionType!"); return; } - string fuze = subMunitionType.fuzeType; - fuze.ToLower(); + string fuze = subMunitionType.fuzeType.ToLower(); + BulletFuzeTypes sFuze; switch (fuze) { @@ -1115,12 +1375,14 @@ public void BeehiveDetonation() sFuze = PooledBullet.BulletFuzeTypes.Impact; break; case "none": - sFuze = PooledBullet.BulletFuzeTypes.None; + sFuze = PooledBullet.BulletFuzeTypes.Impact; break; default: - sFuze = PooledBullet.BulletFuzeTypes.Impact; + sFuze = PooledBullet.BulletFuzeTypes.None; break; } + if (BDArmorySettings.DEBUG_WEAPONS) + Debug.Log("[BDArmory.PooledBullet]: Beehive Detonation: parsing submunition fuze: " + fuze + ", index: " + sFuze); for (int s = 0; s < subMunitionType.subProjectileCount; s++) { GameObject Bullet = ModuleWeapon.bulletPool.GetPooledObject(); @@ -1130,21 +1392,20 @@ public void BeehiveDetonation() pBullet.caliber = subMunitionType.caliber; pBullet.bulletVelocity = subMunitionType.bulletVelocity; pBullet.bulletMass = subMunitionType.bulletMass; - pBullet.explosive = subMunitionType.explosive; pBullet.incendiary = subMunitionType.incendiary; pBullet.apBulletMod = subMunitionType.apBulletMod; pBullet.bulletDmgMult = bulletDmgMult; pBullet.ballisticCoefficient = subMunitionType.bulletMass / (((Mathf.PI * 0.25f * subMunitionType.caliber * subMunitionType.caliber) / 1000000f) * 0.295f); pBullet.timeElapsedSinceCurrentSpeedWasAdjusted = 0; - pBullet.timeToLiveUntil = 4000 / bulletVelocity * 1.1f + Time.time; - Vector3 firedVelocity = VectorUtils.GaussianDirectionDeviation(currentVelocity.normalized, (subMunitionType.subProjectileCount / Mathf.Sqrt((currentVelocity * dragVelocityFactor).magnitude / 10))) * (subMunitionType.bulletVelocity / 10); //more subprojectiles = wider spread, higher base velocity = tighter spread - pBullet.currentVelocity = (GetDragAdjustedVelocity() + Krakensbane.GetFrameVelocityV3f()) + firedVelocity; // use the real velocity, w/o offloading + pBullet.timeToLiveUntil = 2000 / bulletVelocity * 1.1f + Time.time; + Vector3 firedVelocity = VectorUtils.GaussianDirectionDeviation(currentVelocity.normalized, subMunitionType.subProjectileDispersion > 0 ? subMunitionType.subProjectileDispersion : (subMunitionType.subProjectileCount / BDAMath.Sqrt((currentVelocity * dragVelocityFactor).magnitude / 100))) * subMunitionType.bulletVelocity; //more subprojectiles = wider spread, higher base velocity = tighter spread + pBullet.currentVelocity = GetDragAdjustedVelocity() + firedVelocity; // currentVelocity is already the real velocity w/o offloading pBullet.sourceWeapon = sourceWeapon; pBullet.sourceVessel = sourceVessel; pBullet.team = team; pBullet.bulletTexturePath = bulletTexturePath; - pBullet.projectileColor = Utils.ParseColor255(subMunitionType.projectileColor); - pBullet.startColor = Utils.ParseColor255(subMunitionType.startColor); + pBullet.projectileColor = GUIUtils.ParseColor255(subMunitionType.projectileColor); + pBullet.startColor = GUIUtils.ParseColor255(subMunitionType.startColor); pBullet.fadeColor = subMunitionType.fadeColor; pBullet.tracerStartWidth = subMunitionType.caliber / 300; pBullet.tracerEndWidth = subMunitionType.caliber / 750; @@ -1153,11 +1414,25 @@ public void BeehiveDetonation() pBullet.tracerLuminance = tracerLuminance; pBullet.bulletDrop = bulletDrop; - if (subMunitionType.explosive) + if (subMunitionType.tntMass > 0 || subMunitionType.beehive) { pBullet.explModelPath = explModelPath; pBullet.explSoundPath = explSoundPath; pBullet.tntMass = subMunitionType.tntMass; + string HEtype = subMunitionType.explosive.ToLower(); + switch (HEtype) + { + case "standard": + pBullet.HEType = PooledBullet.PooledBulletTypes.Explosive; + break; + //legacy support for older configs that are still explosive = true + case "true": + pBullet.HEType = PooledBullet.PooledBulletTypes.Explosive; + break; + case "shaped": + pBullet.HEType = PooledBullet.PooledBulletTypes.Shaped; + break; + } pBullet.detonationRange = detonationRange; pBullet.maxAirDetonationRange = maxAirDetonationRange; pBullet.defaultDetonationRange = defaultDetonationRange; @@ -1167,6 +1442,7 @@ public void BeehiveDetonation() { pBullet.fuzeType = PooledBullet.BulletFuzeTypes.None; pBullet.sabot = (((((subMunitionType.bulletMass * 1000) / ((subMunitionType.caliber * subMunitionType.caliber * Mathf.PI / 400) * 19) + 1) * 10) > subMunitionType.caliber * 4)) ? true : false; + pBullet.HEType = PooledBullet.PooledBulletTypes.Slug; } pBullet.EMP = subMunitionType.EMP; pBullet.nuclear = subMunitionType.nuclear; @@ -1203,29 +1479,38 @@ private bool ProximityAirDetonation(float distanceFromStart) { bool detonate = false; - if (distanceTraveled <= detonationRange * 2.5f && (fuzeType == BulletFuzeTypes.Proximity || fuzeType == BulletFuzeTypes.Timed)) return false; //bullet not past arming distance - - if (!(((explosive || nuclear) && tntMass > 0) || beehive)) return false; - if (isAPSprojectile && (tgtShell != null || tgtRocket != null)) { if (Vector3.Distance(transform.position, tgtShell != null ? tgtShell.transform.position : tgtRocket.transform.position) < detonationRange / 2) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log("[BDArmory.PooledRocket]: rocket proximity to APS target | Distance overlap = " + detonationRange + "| tgt name = " + tgtShell != null ? tgtShell.name : tgtRocket.name); + if (BDArmorySettings.DEBUG_WEAPONS) + Debug.Log("[BDArmory.PooledBullet]: bullet proximity to APS target | Distance overlap = " + detonationRange + "| tgt name = " + tgtShell != null ? tgtShell.name : tgtRocket.name); return detonate = true; } } + + if (distanceTraveled <= detonationRange * 2.5f && (fuzeType == BulletFuzeTypes.Proximity || fuzeType == BulletFuzeTypes.Timed)) return false; //bullet not past arming distance + + if (!(((HEType != PooledBulletTypes.Slug || nuclear) && tntMass > 0) || beehive)) return false; + + if (fuzeType == BulletFuzeTypes.Timed || fuzeType == BulletFuzeTypes.Flak) { - if (distanceFromStart > (beehive ? maxAirDetonationRange - 100 : maxAirDetonationRange) || distanceFromStart > (beehive ? defaultDetonationRange - 100 : defaultDetonationRange)) + if (distanceFromStart > (beehive ? maxAirDetonationRange - detonationRange : maxAirDetonationRange) || distanceFromStart > (beehive ? defaultDetonationRange - detonationRange : defaultDetonationRange)) { return detonate = true; } } if (fuzeType == BulletFuzeTypes.Proximity || fuzeType == BulletFuzeTypes.Flak) { - using (var hitsEnu = Physics.OverlapSphere(transform.position, detonationRange, (int)(LayerMasks.Parts | LayerMasks.Scenery | LayerMasks.Unknown19)).AsEnumerable().GetEnumerator()) + var layerMask = (int)(LayerMasks.Parts | LayerMasks.Scenery | LayerMasks.Unknown19 | LayerMasks.Wheels); + var overlapSphereColliderCount = Physics.OverlapSphereNonAlloc(transform.position, detonationRange, proximityOverlapSphereColliders, layerMask); + if (overlapSphereColliderCount == proximityOverlapSphereColliders.Length) + { + proximityOverlapSphereColliders = Physics.OverlapSphere(transform.position, detonationRange, layerMask); + overlapSphereColliderCount = proximityOverlapSphereColliders.Length; + } + using (var hitsEnu = proximityOverlapSphereColliders.Take(overlapSphereColliderCount).GetEnumerator()) { while (hitsEnu.MoveNext()) { @@ -1238,7 +1523,7 @@ private bool ProximityAirDetonation(float distanceFromStart) if (ProjectileUtils.IsIgnoredPart(partHit)) continue; // Ignore ignored parts. - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_WEAPONS) Debug.Log("[BDArmory.PooledBullet]: Bullet proximity sphere hit | Distance overlap = " + detonationRange + "| Part name = " + partHit.name); return detonate = true; @@ -1313,38 +1598,45 @@ private bool ExplosiveDetonation(Part hitPart, RaycastHit hit, Ray ray, bool pen /////////////////////////////////////////////////////////////////////// // High Explosive Detonation /////////////////////////////////////////////////////////////////////// - if (fuzeType == BulletFuzeTypes.None) return false; + if (fuzeType == BulletFuzeTypes.None) + { + if (BDArmorySettings.DEBUG_WEAPONS) + { + Debug.Log($"[BDArmory.PooledBullet]: Bullet {bullet.DisplayName} attempted detonation, has improper fuze ({fuzeType}). Fix your bullet config."); + } + return false; + } if (hitPart == null || hitPart.vessel != sourceVessel) { //if bullet hits and is HE, detonate and kill bullet - if ((explosive || nuclear) && tntMass > 0) + if ((HEType != PooledBulletTypes.Slug || nuclear) && tntMass > 0) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_WEAPONS) { Debug.Log("[BDArmory.PooledBullet]: Detonation Triggered | penetration: " + hasPenetrated + " penTick: " + penTicker + " airDet: " + (fuzeType == BulletFuzeTypes.Timed || fuzeType == BulletFuzeTypes.Flak)); } - if (fuzeType == BulletFuzeTypes.Timed || fuzeType == BulletFuzeTypes.Flak) + if ((fuzeType == BulletFuzeTypes.Timed || fuzeType == BulletFuzeTypes.Flak) || HEType == PooledBulletTypes.Shaped) { - if (explosive) - ExplosionFx.CreateExplosion(hit.point, GetExplosivePower(), explModelPath, explSoundPath, ExplosionSourceType.Bullet, caliber, null, sourceVesselName, null, default, -1, false, bulletMass, -1, dmgMult); + if (HEType != PooledBulletTypes.Slug) + ExplosionFx.CreateExplosion(hit.point, GetExplosivePower(), explModelPath, explSoundPath, ExplosionSourceType.Bullet, caliber, null, sourceVesselName, null, HEType == PooledBulletTypes.Explosive ? default : ray.direction, -1, false, bulletMass, -1, dmgMult, HEType == PooledBulletTypes.Shaped ? "shapedcharge" : "standard", hitPart, HEType == PooledBulletTypes.Shaped ? apBulletMod : 1f, ProjectileUtils.isReportingWeapon(sourceWeapon) ? (float)DistanceTraveled : -1); if (nuclear) - { - NukeFX.CreateExplosion(currPosition, ExplosionSourceType.BattleDamage, sourceVesselName, bullet.DisplayName, 0, tntMass * 200, tntMass, tntMass, EMP, blastSoundPath, flashModelPath, shockModelPath, blastModelPath, plumeModelPath, debrisModelPath, "", ""); - } + NukeFX.CreateExplosion(currPosition, ExplosionSourceType.Bullet, sourceVesselName, bullet.DisplayName, 0, tntMass * 200, tntMass, tntMass, EMP, blastSoundPath, flashModelPath, shockModelPath, blastModelPath, plumeModelPath, debrisModelPath, "", ""); } else { - if (explosive) - ExplosionFx.CreateExplosion(hit.point - (ray.direction * 0.1f), GetExplosivePower(), explModelPath, explSoundPath, ExplosionSourceType.Bullet, caliber, null, sourceVesselName, null, default, -1, false, bulletMass, -1, dmgMult, "standard", penetratingHit ? hitPart : null); + if (HEType != PooledBulletTypes.Slug) + ExplosionFx.CreateExplosion(hit.point - (ray.direction * 0.1f), GetExplosivePower(), explModelPath, explSoundPath, ExplosionSourceType.Bullet, caliber, null, sourceVesselName, null, HEType == PooledBulletTypes.Explosive ? default : ray.direction, -1, false, bulletMass, -1, dmgMult, HEType == PooledBulletTypes.Shaped ? "shapedcharge" : "standard", hitPart, HEType == PooledBulletTypes.Shaped ? apBulletMod : 1f, ProjectileUtils.isReportingWeapon(sourceWeapon) ? (float)DistanceTraveled : -1); if (nuclear) - { - NukeFX.CreateExplosion(currPosition, ExplosionSourceType.BattleDamage, sourceVesselName, bullet.DisplayName, 0, tntMass * 200, tntMass, tntMass, EMP, blastSoundPath, flashModelPath, shockModelPath, blastModelPath, plumeModelPath, debrisModelPath, "", ""); - } + NukeFX.CreateExplosion(currPosition, ExplosionSourceType.Bullet, sourceVesselName, bullet.DisplayName, 0, tntMass * 200, tntMass, tntMass, EMP, blastSoundPath, flashModelPath, shockModelPath, blastModelPath, plumeModelPath, debrisModelPath, "", ""); } KillBullet(); hasDetonated = true; return true; } + if (BDArmorySettings.DEBUG_WEAPONS) + { + Debug.Log($"[BDArmory.PooledBullet]: Bullet {bullet.DisplayName} attempted detonation, has no tntmass amount ({tntMass}) or is a solid slug ({HEType}). Fix your bullet config."); + } } return false; } @@ -1373,9 +1665,33 @@ public void UpdateWidth(Camera c, float resizeFactor) public void KillBullet() { + if (HEType == PooledBulletTypes.Slug && partsHit.Count > 0) + { + if (ProjectileUtils.isReportingWeapon(sourceWeapon) && BDACompetitionMode.Instance.competitionIsActive) + { + string msg = $"{partsHit[0].vessel.GetName()} was nailed by {sourceVesselName}'s {sourceWeapon.partInfo.title} at {initialHitDistance:F3}m, damaging {partsHit.Count} parts."; + //string message = $"{partsHit[0].vessel.GetName()} was nailed by {sourceVesselName}'s {bullet.DisplayName} at {initialHitDistance:F3}, damaging {partsHit.Count} parts."; + BDACompetitionMode.Instance.competitionStatus.Add(msg); + } + } gameObject.SetActive(false); } + public void SetTracerPosition() + { + // visual tracer velocity is relative to the observer (which uses srf_vel when below 100km (f*&king KSP!), not orb_vel) + if (tracerLength == 0) + { + linePositions[0] = transform.position + ((currentVelocity - FlightGlobals.ActiveVessel.Velocity()) * tracerDeltaFactor * 0.45f * Time.fixedDeltaTime); + } + else + { + linePositions[0] = transform.position + ((currentVelocity - FlightGlobals.ActiveVessel.Velocity()).normalized * tracerLength); + } + linePositions[1] = transform.position; + bulletTrail.SetPositions(linePositions); + } + void FadeColor() { Vector4 endColorV = new Vector4(projectileColor.r, projectileColor.g, projectileColor.b, projectileColor.a); @@ -1390,7 +1706,7 @@ bool RicochetOnPart(Part p, RaycastHit hit, float angleFromNormal, float impactV //15 degrees should virtually guarantee a ricochet, but 75 degrees should nearly always be fine float chance = (((angleFromNormal - 5) / 75) * (hitTolerance / 150)) * 100 / Mathf.Clamp01(impactVel / 600); float random = UnityEngine.Random.Range(0f, 100f); - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.PooledBullet]: Ricochet chance: " + chance); + if (BDArmorySettings.DEBUG_WEAPONS) Debug.Log("[BDArmory.PooledBullet]: Ricochet chance: " + chance); if (random < chance) { DoRicochet(p, hit, angleFromNormal, fractionOfDistance, period); diff --git a/BDArmory/Bullets/PooledRocket.cs b/BDArmory/Ammo/PooledRocket.cs similarity index 52% rename from BDArmory/Bullets/PooledRocket.cs rename to BDArmory/Ammo/PooledRocket.cs index e281d9b6a..dc14243c8 100644 --- a/BDArmory/Bullets/PooledRocket.cs +++ b/BDArmory/Ammo/PooledRocket.cs @@ -1,16 +1,18 @@ using System; using System.Collections.Generic; +using UniLinq; +using UnityEngine; + +using BDArmory.Armor; using BDArmory.Competition; -using BDArmory.Core; -using BDArmory.Core.Extension; -using BDArmory.Core.Module; -using BDArmory.Core.Utils; +using BDArmory.Damage; +using BDArmory.Extensions; using BDArmory.FX; -using BDArmory.Misc; -using BDArmory.Modules; +using BDArmory.Weapons; +using BDArmory.Settings; using BDArmory.UI; -using UniLinq; -using UnityEngine; +using BDArmory.Utils; +using static BDArmory.Bullets.PooledBullet; namespace BDArmory.Bullets { @@ -21,11 +23,13 @@ public class PooledRocket : MonoBehaviour public Transform spawnTransform; public Vessel sourceVessel; + public Part sourceWeapon; public string sourceVesselName; public string team; public string rocketName; public float rocketMass; public float caliber; + public float apMod; public float thrust; private Vector3 thrustVector; private Vector3 dragVector; @@ -43,6 +47,8 @@ public class PooledRocket : MonoBehaviour public bool incendiary; public float detonationRange; public float tntMass; + public bool beehive; + public string subMunitionType; bool explosive = true; public float bulletDmgMult = 1; public float dmgMult = 1; @@ -53,10 +59,18 @@ public class PooledRocket : MonoBehaviour public string explModelPath; public string explSoundPath; + public bool nuclear; + public string flashModelPath; + public string shockModelPath; + public string blastModelPath; + public string plumeModelPath; + public string debrisModelPath; + public string blastSoundPath; + public string rocketSoundPath; float startTime; - float lifeTime = 10; + public float lifeTime; Vector3 prevPosition; public Vector3 currPosition; @@ -64,14 +78,14 @@ public class PooledRocket : MonoBehaviour bool startUnderwater = false; Ray RocketRay; private float impactVelocity; - public Vector3 currentVelocity = Vector3.zero; + public Vector3 currentVelocity = Vector3.zero; // Current real velocity w/o offloading public bool hasPenetrated = false; public bool hasDetonated = false; public int penTicker = 0; private Part CurrentPart = null; - private int collisionLayerMask = (int)(LayerMasks.Parts | LayerMasks.Scenery | LayerMasks.EVA | LayerMasks.Unknown19 | LayerMasks.Unknown23); // Why 19 and 23? - private int explosionLayerMask = (int)(LayerMasks.Parts | LayerMasks.Scenery | LayerMasks.Unknown19); // Why 19 and not EVA? + private const int collisionLayerMask = (int)(LayerMasks.Parts | LayerMasks.Scenery | LayerMasks.EVA | LayerMasks.Unknown19 | LayerMasks.Unknown23 | LayerMasks.Wheels); // Why 19 and 23? + private const int explosionLayerMask = (int)(LayerMasks.Parts | LayerMasks.Scenery | LayerMasks.Unknown19 | LayerMasks.Wheels); // Why 19 and not EVA? private float distanceFromStart = 0; @@ -90,7 +104,9 @@ public class PooledRocket : MonoBehaviour public AudioSource audioSource; - HashSet craftHit = new HashSet(); + static RaycastHit[] hits = new RaycastHit[10]; + static Collider[] proximityOverlapSphereColliders = new Collider[10]; + static Collider[] detonateOverlapSphereColliders = new Collider[10]; void OnEnable() { BDArmorySetup.numberOfParticleEmitters++; @@ -171,10 +187,32 @@ void OnEnable() { HERatio = 0; } - if (caliber > 60) + if (caliber >= BDArmorySettings.APS_THRESHOLD) //if (caliber > 60) { BDATargetManager.FiredRockets.Add(this); } + if (nuclear) + { + var nuke = sourceWeapon.FindModuleImplementing(); + if (nuke == null) + { + flashModelPath = BDModuleNuke.defaultflashModelPath; + shockModelPath = BDModuleNuke.defaultShockModelPath; + blastModelPath = BDModuleNuke.defaultBlastModelPath; + plumeModelPath = BDModuleNuke.defaultPlumeModelPath; + debrisModelPath = BDModuleNuke.defaultDebrisModelPath; + blastSoundPath = BDModuleNuke.defaultBlastSoundPath; + } + else + { + flashModelPath = nuke.flashModelPath; + shockModelPath = nuke.shockModelPath; + blastModelPath = nuke.blastModelPath; + plumeModelPath = nuke.plumeModelPath; + debrisModelPath = nuke.debrisModelPath; + blastSoundPath = nuke.blastSoundPath; + } + } } void OnDisable() @@ -196,7 +234,7 @@ void OnDisable() sourceVesselName = null; spawnTransform = null; CurrentPart = null; - if (caliber > 60) + if (caliber >= BDArmorySettings.APS_THRESHOLD) //if (caliber > 60) { BDATargetManager.FiredRockets.Remove(this); } @@ -213,11 +251,11 @@ void FixedUpdate() return; } //floating origin and velocity offloading corrections - if (!FloatingOrigin.Offset.IsZero() || !Krakensbane.GetFrameVelocity().IsZero()) + if (BDKrakensbane.IsActive) { - transform.position -= FloatingOrigin.OffsetNonKrakensbane; - prevPosition -= FloatingOrigin.OffsetNonKrakensbane; - startPosition -= FloatingOrigin.OffsetNonKrakensbane; + transform.position -= BDKrakensbane.FloatingOriginOffsetNonKrakensbane; + prevPosition -= BDKrakensbane.FloatingOriginOffsetNonKrakensbane; + startPosition -= BDKrakensbane.FloatingOriginOffsetNonKrakensbane; } distanceFromStart = Vector3.Distance(transform.position, startPosition); @@ -225,7 +263,7 @@ void FixedUpdate() { transform.parent = null; rb.isKinematic = false; - rb.velocity = parentRB.velocity + Krakensbane.GetFrameVelocityV3f(); + rb.velocity = parentRB.velocity + BDKrakensbane.FrameVelocityV3f; } if (rb && !rb.isKinematic) @@ -243,7 +281,7 @@ void FixedUpdate() //model transform. always points prograde transform.rotation = Quaternion.RotateTowards(transform.rotation, - Quaternion.LookRotation(rb.velocity + Krakensbane.GetFrameVelocity(), transform.up), + Quaternion.LookRotation(rb.velocity, transform.up), Mathf.Clamp01(atmosMultiplier * 2.5f) * (0.5f * (Time.time - startTime)) * 50 * Time.fixedDeltaTime); @@ -265,7 +303,7 @@ void FixedUpdate() //rb.AddRelativeForce(dragVector); //Debug.Log("[ROCKETDRAG] current vel: " + rb.velocity.magnitude.ToString("0.0") + "; current dragforce: " + dragVector.magnitude + "; current atm density: " + atmosMultiplier.ToString("0.00")); } - currentVelocity = rb.velocity; + currentVelocity = rb.velocity; // The rb.velocity is w/o offloading here, since rockets aren't vessels. } if (Time.time - startTime > thrustTime) @@ -297,10 +335,15 @@ void FixedUpdate() currPosition = transform.position; float dist = (currPosition - prevPosition).magnitude; RocketRay = new Ray(prevPosition, currPosition - prevPosition); - var hits = Physics.RaycastAll(RocketRay, dist, collisionLayerMask); - if (hits.Length > 0) + var hitCount = Physics.RaycastNonAlloc(RocketRay, hits, dist, collisionLayerMask); + if (hitCount == hits.Length) // If there's a whole bunch of stuff in the way (unlikely), then we need to increase the size of our hits buffer. { - var orderedHits = hits.OrderBy(x => x.distance); + hits = Physics.RaycastAll(RocketRay, dist, collisionLayerMask); + hitCount = hits.Length; + } + if (hitCount > 0) + { + var orderedHits = hits.Take(hitCount).OrderBy(x => x.distance); using (var hitsEnu = orderedHits.GetEnumerator()) { @@ -324,14 +367,15 @@ void FixedUpdate() } if (hitPart != null && ProjectileUtils.IsIgnoredPart(hitPart)) continue; // Ignore ignored parts. - if (hitPart != null && (hitPart == CurrentPart && CurrentPart.name.Contains("Armor"))) continue; //only have bullet hit armor panels once - no back armor to hit if penetration + if (hitPart != null && (hitPart == CurrentPart && ProjectileUtils.IsArmorPart(CurrentPart))) continue; //only have bullet hit armor panels once - no back armor to hit if penetration + CurrentPart = hitPart; if (hitEVA != null) { hitPart = hitEVA.part; // relative velocity, separate from the below statement, because the hitpart might be assigned only above if (hitPart.rb != null) - impactVelocity = (rb.velocity - (hitPart.rb.velocity + Krakensbane.GetFrameVelocityV3f())).magnitude; + impactVelocity = (rb.velocity - (hitPart.rb.velocity + BDKrakensbane.FrameVelocityV3f)).magnitude; else impactVelocity = rb.velocity.magnitude; if (dmgMult < 0) @@ -342,7 +386,7 @@ void FixedUpdate() { ProjectileUtils.ApplyDamage(hitPart, hit, dmgMult, 1, caliber, rocketMass * 1000, impactVelocity, bulletDmgMult, distanceFromStart, explosive, incendiary, false, sourceVessel, rocketName, team, ExplosionSourceType.Rocket, true, true, true); } - ProjectileUtils.StealResources(hitPart, sourceVessel, thief); + ResourceUtils.StealResources(hitPart, sourceVessel, thief); Detonate(hit.point, false); return; } @@ -353,13 +397,13 @@ void FixedUpdate() if (hitPart != null && hitPart.rb != null) // using relative velocity vector instead of just rocket velocity // since KSP vessels can easily be moving faster than rockets - impactVector = rb.velocity - (hitPart.rb.velocity + Krakensbane.GetFrameVelocityV3f()); + impactVector = rb.velocity - (hitPart.rb.velocity + BDKrakensbane.FrameVelocityV3f); float hitAngle = Vector3.Angle(impactVector, -hit.normal); if (ProjectileUtils.CheckGroundHit(hitPart, hit, caliber)) { - ProjectileUtils.CheckBuildingHit(hit, rocketMass * 1000, rb.velocity, bulletDmgMult); + if (!BDArmorySettings.PAINTBALL_MODE) ProjectileUtils.CheckBuildingHit(hit, rocketMass * 1000, rb.velocity, bulletDmgMult); Detonate(hit.point, false); return; } @@ -399,17 +443,43 @@ void FixedUpdate() float Strength = Armor.Strength; float safeTemp = Armor.SafeUseTemp; float Density = Armor.Density; + float vFactor = Armor.vFactor; + float muParam1 = Armor.muParam1; + float muParam2 = Armor.muParam2; + float muParam3 = Armor.muParam3; + + if (hitPart.skinTemperature > safeTemp) //has the armor started melting/denaturing/whatever? + { + //vFactor *= 1/(1.25f*0.75f-0.25f*0.75f*0.75f); + vFactor *= 1.25490196078f; // Uses the above equation but just calculated out. + // The equation 1/(1.25*x-0.25*x^2) approximates the effect of changing yield strength + // by a factor of x + if (hitPart.skinTemperature > safeTemp * 1.5f) + { + vFactor *= 1.77777777778f; // Same as used above, but here with x = 0.5. Maybe this should be + // some kind of a curve? + } + } + int armorType = (int)Armor.ArmorTypeNum; - if (BDArmorySettings.DRAW_ARMOR_LABELS) + if (BDArmorySettings.DEBUG_ARMOR) { - Debug.Log("[PooledBUllet].ArmorVars found: Strength : " + Strength + "; Ductility: " + Ductility + "; Hardness: " + hardness + "; MaxTemp: " + safeTemp + "; Density: " + Density); + Debug.Log("[BDArmory.PooledBUllet]: ArmorVars found: Strength : " + Strength + "; Ductility: " + Ductility + "; Hardness: " + hardness + "; MaxTemp: " + safeTemp + "; Density: " + Density); } float bulletEnergy = ProjectileUtils.CalculateProjectileEnergy(rocketMass * 1000, impactVelocity); float armorStrength = ProjectileUtils.CalculateArmorStrength(caliber, thickness, Ductility, Strength, Density, safeTemp, hitPart); //calculate bullet deformation - float newCaliber = ProjectileUtils.CalculateDeformation(armorStrength, bulletEnergy, caliber, impactVelocity, hardness, Density, HERatio, 1); + float newCaliber = ProjectileUtils.CalculateDeformation(armorStrength, bulletEnergy, caliber, impactVelocity, hardness, Density, HERatio, 1, false); //calculate penetration - penetration = ProjectileUtils.CalculatePenetration(caliber, newCaliber, rocketMass * 1000, impactVelocity, Ductility, Density, Strength, thickness, 1); + /*if (Ductility > 0.05) + {*/ + penetration = ProjectileUtils.CalculatePenetration(caliber, impactVelocity, rocketMass * 1000f, apMod, Strength, vFactor, muParam1, muParam2, muParam3); + /*} + else + { + penetration = ProjectileUtils.CalculateCeramicPenetration(caliber, newCaliber, rocketMass * 1000, impactVelocity, Ductility, Density, Strength, thickness, 1); + }*/ + caliber = newCaliber; //update bullet with new caliber post-deformation(if any) penetrationFactor = ProjectileUtils.CalculateArmorPenetration(hitPart, penetration, thickness); @@ -448,7 +518,7 @@ void FixedUpdate() } else { - Debug.Log("[PooledBUllet].ArmorVars not found; hitPart null"); + Debug.Log("[BDArmory.PooledRocket]: ArmorVars not found; hitPart null"); } if (penetration > thickness) { @@ -467,7 +537,7 @@ void FixedUpdate() } else { - float cockpitPen = (float)(16f * impactVelocity * Mathf.Sqrt(rocketMass) / Mathf.Sqrt(caliber)); + float cockpitPen = (float)(16f * impactVelocity * BDAMath.Sqrt(rocketMass) / BDAMath.Sqrt(caliber)); if (cockpitPen > Mathf.Max(20 / anglemultiplier, 1)) ProjectileUtils.ApplyDamage(hitPart, hit, dmgMult, penetrationFactor, caliber, rocketMass * 1000, impactVelocity, bulletDmgMult, distanceFromStart, explosive, incendiary, false, sourceVessel, rocketName, team, ExplosionSourceType.Rocket, penTicker > 0 ? false : true, penTicker > 0 ? false : true, (cockpitPen > Mathf.Max(20 / anglemultiplier, 1)) ? true : false); if (!explosive) @@ -475,7 +545,7 @@ void FixedUpdate() BDACompetitionMode.Instance.Scores.RegisterRocketStrike(sourceVesselName, hitPart.vessel.GetName()); //if non-explosive hit, add rocketstrike, else ExplosionFX adds rocketstrike from HE detonation } } - ProjectileUtils.StealResources(hitPart, sourceVessel, thief); + ResourceUtils.StealResources(hitPart, sourceVessel, thief); penTicker += 1; //ProjectileUtils.CheckPartForExplosion(hitPart); @@ -500,7 +570,7 @@ void FixedUpdate() hitPart.rb.AddForceAtPosition(impactVector.normalized * accelerationMagnitude, hit.point, ForceMode.Acceleration); - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_WEAPONS) Debug.Log("[BDArmory.PooledRocket]: Force Applied " + Math.Round(accelerationMagnitude, 2) + "| Vessel mass in kgs=" + hitPart.vessel.GetTotalMass() * 1000 + "| rocket effective mass =" + rocketMass * 1000); } @@ -521,7 +591,7 @@ void FixedUpdate() if (rb.velocity.magnitude <= 100 && hasPenetrated && (Time.time - startTime > thrustTime)) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_WEAPONS) { Debug.Log("[BDArmory.PooledRocket]: Rocket ballistic velocity too low, stopping"); } @@ -552,11 +622,11 @@ void FixedUpdate() } prevPosition = currPosition; - if (Time.time - startTime > lifeTime) // life's 10s, quite a long time for faster rockets + if (Time.time - startTime > lifeTime) { Detonate(transform.position, true); } - if (distanceFromStart >= maxAirDetonationRange)//rockets are performance intensive, lets cull those that have flown too far away + if (distanceFromStart >= (beehive ? maxAirDetonationRange - 100 : maxAirDetonationRange))//rockets are performance intensive, lets cull those that have flown too far away { Detonate(transform.position, false); } @@ -570,13 +640,29 @@ private bool ProximityAirDetonation(float distanceFromStart) { bool detonate = false; + if (isAPSprojectile && (tgtShell != null || tgtRocket != null)) + { + if (Vector3.Distance(transform.position, tgtShell != null ? tgtShell.transform.position : tgtRocket.transform.position) < detonationRange / 2) + { + if (BDArmorySettings.DEBUG_WEAPONS) + Debug.Log("[BDArmory.PooledRocket]: rocket proximity to APS target | Distance overlap = " + detonationRange + "| tgt name = " + tgtShell != null ? tgtShell.name : tgtRocket.name); + return detonate = true; + } + } + if (distanceFromStart <= blastRadius * 2) return false; - if (!explosive || tntMass <= 0) return false; + if (!(((explosive || nuclear) && tntMass > 0) || beehive)) return false; if (flak) { - using (var hitsEnu = Physics.OverlapSphere(transform.position, detonationRange, explosionLayerMask).AsEnumerable().GetEnumerator()) + var overlapSphereColliderCount = Physics.OverlapSphereNonAlloc(transform.position, detonationRange, proximityOverlapSphereColliders, explosionLayerMask); + if (overlapSphereColliderCount == proximityOverlapSphereColliders.Length) + { + proximityOverlapSphereColliders = Physics.OverlapSphere(transform.position, detonationRange, explosionLayerMask); + overlapSphereColliderCount = proximityOverlapSphereColliders.Length; + } + using (var hitsEnu = proximityOverlapSphereColliders.Take(overlapSphereColliderCount).GetEnumerator()) { while (hitsEnu.MoveNext()) { @@ -592,8 +678,8 @@ private bool ProximityAirDetonation(float distanceFromStart) BDACompetitionMode.Instance.Scores.RegisterRocketHit(aName, tName, 1); - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log("[BDArmory.PooledRocket]: rocket proximity sphere hit | Distance overlap = " + detonationRange + "| Part name = " + partHit.name); + if (BDArmorySettings.DEBUG_WEAPONS) + Debug.Log($"[BDArmory.PooledRocket]: rocket proximity sphere hit | Distance overlap = {detonationRange} | Part name = {partHit.name} on {partHit.vessel.vesselName}"); return detonate = true; } catch (Exception e) @@ -602,15 +688,6 @@ private bool ProximityAirDetonation(float distanceFromStart) } } } - if (isAPSprojectile && (tgtShell != null || tgtRocket != null)) - { - if (Vector3.Distance(transform.position, tgtShell != null ? tgtShell.transform.position : tgtRocket.transform.position) < detonationRange / 2) - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log("[BDArmory.PooledRocket]: rocket proximity to APS target | Distance overlap = " + detonationRange + "| tgt name = " + tgtShell != null ? tgtShell.name : tgtRocket.name); - return detonate = true; - } - } } return detonate; } @@ -644,116 +721,332 @@ void Detonate(Vector3 pos, bool missed, Part PenetratingHit = null) { if (!missed) { - if (tntMass > 0) + if (beehive) { - Vector3 direction = default(Vector3); - if (shaped) - { - direction = (pos + rb.velocity * Time.deltaTime).normalized; - } - if (gravitic) + BeehiveDetonation(); + } + else + { + if (tntMass > 0) { - using (var hitsEnu = Physics.OverlapSphere(transform.position, blastRadius, explosionLayerMask).AsEnumerable().GetEnumerator()) + Vector3 direction = default(Vector3); + if (shaped) + { + direction = rb.velocity.normalized; + //direction = transform.forward //ideal, but no guarantee that mod rockets have correct transform orientation + } + if (gravitic) { - while (hitsEnu.MoveNext()) + var overlapSphereColliderCount = Physics.OverlapSphereNonAlloc(transform.position, blastRadius, detonateOverlapSphereColliders, explosionLayerMask); + if (overlapSphereColliderCount == detonateOverlapSphereColliders.Length) { - if (hitsEnu.Current == null) continue; - - Part partHit = hitsEnu.Current.GetComponentInParent(); - if (partHit == null) continue; - if (ProjectileUtils.IsIgnoredPart(partHit)) continue; // Ignore ignored parts. - float distance = Vector3.Distance(transform.position, partHit.transform.position); - if (gravitic) + detonateOverlapSphereColliders = Physics.OverlapSphere(transform.position, blastRadius, explosionLayerMask); + overlapSphereColliderCount = detonateOverlapSphereColliders.Length; + } + using (var hitsEnu = detonateOverlapSphereColliders.Take(overlapSphereColliderCount).GetEnumerator()) + { + while (hitsEnu.MoveNext()) { - if (partHit.mass > 0) + if (hitsEnu.Current == null) continue; + + Part partHit = hitsEnu.Current.GetComponentInParent(); + if (partHit == null) continue; + if (ProjectileUtils.IsIgnoredPart(partHit)) continue; // Ignore ignored parts. + float distance = Vector3.Distance(transform.position, partHit.transform.position); + if (gravitic) { - var ME = partHit.vessel.rootPart.FindModuleImplementing(); - if (ME == null) + if (partHit.mass > 0) { - ME = (ModuleMassAdjust)partHit.vessel.rootPart.AddModule("ModuleMassAdjust"); + var ME = partHit.vessel.rootPart.FindModuleImplementing(); + if (ME == null) + { + ME = (ModuleMassAdjust)partHit.vessel.rootPart.AddModule("ModuleMassAdjust"); + } + ME.massMod += (massMod * (1 - (distance / blastRadius))); //this way craft at edge of blast might only get disabled instead of bricked + ME.duration += (BDArmorySettings.WEAPON_FX_DURATION * (1 - (distance / blastRadius))); //can bypass EMP damage cap } - ME.massMod += (massMod * (1 - (distance / blastRadius))); //this way craft at edge of blast might only get disabled instead of bricked - ME.duration += (BDArmorySettings.WEAPON_FX_DURATION * (1 - (distance / blastRadius))); //can bypass EMP damage cap } } } } - } - if (incendiary) - { - for (int f = 0; f < 20; f++) //throw 20 random raytraces out in a sphere and see what gets tagged + if (incendiary) { - Ray LoSRay = new Ray(transform.position, VectorUtils.GaussianDirectionDeviation(transform.forward, 170)); - RaycastHit hit; - if (Physics.Raycast(LoSRay, out hit, blastRadius * 1.2f, collisionLayerMask)) // only add fires to parts in LoS of blast + for (int f = 0; f < 20; f++) //throw 20 random raytraces out in a sphere and see what gets tagged { - KerbalEVA eva = hit.collider.gameObject.GetComponentUpwards(); - Part p = eva ? eva.part : hit.collider.gameObject.GetComponentInParent(); - float distance = Vector3.Distance(transform.position, hit.point); - if (p != null) + Ray LoSRay = new Ray(transform.position, VectorUtils.GaussianDirectionDeviation(transform.forward, 170)); + RaycastHit hit; + if (Physics.Raycast(LoSRay, out hit, blastRadius * 1.2f, collisionLayerMask)) // only add fires to parts in LoS of blast { - BulletHitFX.AttachFire(hit.point, p, caliber, sourceVesselName, BDArmorySettings.WEAPON_FX_DURATION * (1 - (distance / blastRadius)), 1, true); //else apply fire to occluding part - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log("[BDArmory.Rocket]: Applying fire to " + p.name + " at distance " + distance + "m, for " + BDArmorySettings.WEAPON_FX_DURATION * (1 - (distance / blastRadius)) + " seconds"); ; + KerbalEVA eva = hit.collider.gameObject.GetComponentUpwards(); + Part p = eva ? eva.part : hit.collider.gameObject.GetComponentInParent(); + float distance = Vector3.Distance(transform.position, hit.point); + if (p != null) + { + BulletHitFX.AttachFire(hit.point, p, caliber, sourceVesselName, BDArmorySettings.WEAPON_FX_DURATION * (1 - (distance / blastRadius)), 1, true); //else apply fire to occluding part + if (BDArmorySettings.DEBUG_WEAPONS) + Debug.Log("[BDArmory.Rocket]: Applying fire to " + p.name + " at distance " + distance + "m, for " + BDArmorySettings.WEAPON_FX_DURATION * (1 - (distance / blastRadius)) + " seconds"); ; + } + if (BDArmorySettings.DEBUG_WEAPONS) + Debug.Log("[Rocket] incendiary raytrace: " + hit.point.x + "; " + hit.point.y + "; " + hit.point.z); } } - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log("[Rocket] incendiary raytrace: " + hit.point.x + "; " + hit.point.y + "; " + hit.point.z); } - } - if (concussion || EMP || choker) - { - using (var hitsEnu = Physics.OverlapSphere(transform.position, 25, explosionLayerMask).AsEnumerable().GetEnumerator()) + if (concussion || EMP || choker) { - var craftHit = new HashSet(); - while (hitsEnu.MoveNext()) + var overlapSphereColliderCount = Physics.OverlapSphereNonAlloc(transform.position, 25, detonateOverlapSphereColliders, explosionLayerMask); + if (overlapSphereColliderCount == detonateOverlapSphereColliders.Length) + { + detonateOverlapSphereColliders = Physics.OverlapSphere(transform.position, 25, explosionLayerMask); + overlapSphereColliderCount = detonateOverlapSphereColliders.Length; + } + using (var hitsEnu = detonateOverlapSphereColliders.Take(overlapSphereColliderCount).GetEnumerator()) { - if (hitsEnu.Current == null) continue; - if (hitsEnu.Current.gameObject == FlightGlobals.currentMainBody.gameObject) continue; // Ignore terrain hits. - Part partHit = hitsEnu.Current.GetComponentInParent(); - if (partHit == null) continue; - if (ProjectileUtils.IsIgnoredPart(partHit)) continue; // Ignore ignored parts. - if (craftHit.Contains(partHit.vessel)) continue; // Don't hit the same craft multiple times. - craftHit.Add(partHit.vessel); - - float Distance = Vector3.Distance(partHit.transform.position, this.transform.position); - if (partHit != null) + var craftHit = new HashSet(); + while (hitsEnu.MoveNext()) { - if (concussion && partHit.mass > 0) + if (hitsEnu.Current == null) continue; + if (hitsEnu.Current.gameObject == FlightGlobals.currentMainBody.gameObject) continue; // Ignore terrain hits. + Part partHit = hitsEnu.Current.GetComponentInParent(); + if (partHit == null) continue; + if (ProjectileUtils.IsIgnoredPart(partHit)) continue; // Ignore ignored parts. + if (craftHit.Contains(partHit.vessel)) continue; // Don't hit the same craft multiple times. + craftHit.Add(partHit.vessel); + + float Distance = Vector3.Distance(partHit.transform.position, this.transform.position); + if (partHit != null) { - partHit.rb.AddForceAtPosition((partHit.transform.position - this.transform.position).normalized * impulse, partHit.transform.position, ForceMode.Acceleration); - } - if (EMP) - { - var MDEC = partHit.vessel.rootPart.FindModuleImplementing(); - if (MDEC == null) + if (concussion && partHit.mass > 0) { - MDEC = (ModuleDrainEC)partHit.vessel.rootPart.AddModule("ModuleDrainEC"); + partHit.rb.AddForceAtPosition((partHit.transform.position - this.transform.position).normalized * impulse, partHit.transform.position, ForceMode.Acceleration); } - MDEC.incomingDamage = ((25 - Distance) * 5); //this way craft at edge of blast might only get disabled instead of bricked - MDEC.softEMP = false; //can bypass EMP damage cap - } - if (choker) - { - var ash = partHit.vessel.rootPart.FindModuleImplementing(); - if (ash == null) + if (EMP) { - ash = (ModuleDrainIntakes)partHit.vessel.rootPart.AddModule("ModuleDrainIntakes"); + var MDEC = partHit.vessel.rootPart.FindModuleImplementing(); + if (MDEC == null) + { + MDEC = (ModuleDrainEC)partHit.vessel.rootPart.AddModule("ModuleDrainEC"); + } + MDEC.incomingDamage = ((25 - Distance) * 5); //this way craft at edge of blast might only get disabled instead of bricked + MDEC.softEMP = false; //can bypass EMP damage cap + } + if (choker) + { + var ash = partHit.vessel.rootPart.FindModuleImplementing(); + if (ash == null) + { + ash = (ModuleDrainIntakes)partHit.vessel.rootPart.AddModule("ModuleDrainIntakes"); + } + ash.drainDuration += BDArmorySettings.WEAPON_FX_DURATION * (1 - (Distance / 25)); //reduce intake knockout time based on distance from epicenter } - ash.drainDuration += BDArmorySettings.WEAPON_FX_DURATION * (1 - (Distance / 25)); //reduce intake knockout time based on distance from epicenter } } } + ExplosionFx.CreateExplosion(pos, tntMass, explModelPath, explSoundPath, ExplosionSourceType.Rocket, caliber, null, sourceVesselName, null, direction, -1, true); + } + else + { + if (nuclear) + NukeFX.CreateExplosion(currPosition, ExplosionSourceType.Rocket, sourceVesselName, rocket.DisplayName, 0, tntMass * 200, tntMass, tntMass, EMP, blastSoundPath, flashModelPath, shockModelPath, blastModelPath, plumeModelPath, debrisModelPath, "", ""); + else + ExplosionFx.CreateExplosion(pos, tntMass, explModelPath, explSoundPath, ExplosionSourceType.Rocket, caliber, null, sourceVesselName, null, direction, -1, false, rocketMass * 1000, -1, dmgMult, shaped ? "shapedcharge" : "standard", PenetratingHit, apMod, ProjectileUtils.isReportingWeapon(sourceWeapon) ? (float)distanceFromStart : -1); + } + } + } + } + gameObject.SetActive(false); + } + + public void BeehiveDetonation() + { + if (subMunitionType == null) + { + Debug.Log("[BDArmory.PooledBullet] Beehive round not configured with subMunitionType!"); + return; + } + if (BulletInfo.bulletNames.Contains(subMunitionType)) + { + BulletInfo sBullet = BulletInfo.bullets[subMunitionType]; + string fuze = sBullet.fuzeType; + fuze.ToLower(); + BulletFuzeTypes sFuze; + switch (fuze) + { + case "timed": + sFuze = PooledBullet.BulletFuzeTypes.Timed; + break; + case "proximity": + sFuze = PooledBullet.BulletFuzeTypes.Proximity; + break; + case "flak": + sFuze = PooledBullet.BulletFuzeTypes.Flak; + break; + case "delay": + sFuze = PooledBullet.BulletFuzeTypes.Delay; + break; + case "penetrating": + sFuze = PooledBullet.BulletFuzeTypes.Penetrating; + break; + case "impact": + sFuze = PooledBullet.BulletFuzeTypes.Impact; + break; + case "none": + sFuze = PooledBullet.BulletFuzeTypes.None; + break; + default: + sFuze = PooledBullet.BulletFuzeTypes.Impact; + break; + } + for (int s = 0; s < sBullet.subProjectileCount; s++) + { + GameObject Bullet = ModuleWeapon.bulletPool.GetPooledObject(); + PooledBullet pBullet = Bullet.GetComponent(); + pBullet.transform.position = currPosition; + + pBullet.caliber = sBullet.caliber; + pBullet.bulletVelocity = sBullet.bulletVelocity; + pBullet.bulletMass = sBullet.bulletMass; + pBullet.incendiary = sBullet.incendiary; + pBullet.apBulletMod = sBullet.apBulletMod; + pBullet.bulletDmgMult = bulletDmgMult; + pBullet.ballisticCoefficient = sBullet.bulletMass / (((Mathf.PI * 0.25f * sBullet.caliber * sBullet.caliber) / 1000000f) * 0.295f); + pBullet.timeElapsedSinceCurrentSpeedWasAdjusted = 0; + pBullet.timeToLiveUntil = 2000 / sBullet.bulletVelocity * 1.1f + Time.time; + Vector3 firedVelocity = VectorUtils.GaussianDirectionDeviation(currentVelocity.normalized, sBullet.subProjectileDispersion > 0 ? sBullet.subProjectileDispersion : (sBullet.subProjectileCount / BDAMath.Sqrt(currentVelocity.magnitude / 100))) * sBullet.bulletVelocity; //more subprojectiles = wider spread, higher base velocity = tighter spread + pBullet.currentVelocity = currentVelocity + firedVelocity; // currentVelocity is already the real velocity w/o offloading + pBullet.sourceWeapon = sourceWeapon; + pBullet.sourceVessel = sourceVessel; + pBullet.team = team; + pBullet.bulletTexturePath = "BDArmory/Textures/bullet"; + pBullet.projectileColor = GUIUtils.ParseColor255(sBullet.projectileColor); + pBullet.startColor = GUIUtils.ParseColor255(sBullet.startColor); + pBullet.fadeColor = sBullet.fadeColor; + pBullet.tracerStartWidth = sBullet.caliber / 300; + pBullet.tracerEndWidth = sBullet.caliber / 750; + pBullet.tracerLength = 0; + pBullet.tracerDeltaFactor = 2.65f; + pBullet.tracerLuminance = 1.75f; + pBullet.bulletDrop = true; + + if (sBullet.tntMass > 0 || sBullet.beehive) + { + pBullet.explModelPath = explModelPath; + pBullet.explSoundPath = explSoundPath; + pBullet.tntMass = sBullet.tntMass; + string HEtype = sBullet.explosive; + HEtype.ToLower(); + switch (HEtype) + { + case "standard": + pBullet.HEType = PooledBullet.PooledBulletTypes.Explosive; + break; + //legacy support for older configs that are still explosive = true + case "true": + pBullet.HEType = PooledBullet.PooledBulletTypes.Explosive; + break; + case "shaped": + pBullet.HEType = PooledBullet.PooledBulletTypes.Shaped; + break; } - ExplosionFx.CreateExplosion(pos, tntMass, explModelPath, explSoundPath, ExplosionSourceType.Rocket, caliber, null, sourceVesselName, null, direction, -1, true); + pBullet.detonationRange = detonationRange; + pBullet.maxAirDetonationRange = maxAirDetonationRange; + pBullet.defaultDetonationRange = 1000; + pBullet.fuzeType = sFuze; } else { - ExplosionFx.CreateExplosion(pos, tntMass, explModelPath, explSoundPath, ExplosionSourceType.Rocket, caliber, null, sourceVesselName, null, direction, -1, false, rocketMass * 1000, -1, dmgMult, shaped ? "shapedcharge" : "standard", PenetratingHit); + pBullet.fuzeType = PooledBullet.BulletFuzeTypes.None; + pBullet.sabot = (((((sBullet.bulletMass * 1000) / ((sBullet.caliber * sBullet.caliber * Mathf.PI / 400) * 19) + 1) * 10) > sBullet.caliber * 4)) ? true : false; + pBullet.HEType = PooledBullet.PooledBulletTypes.Slug; } + pBullet.EMP = sBullet.EMP; + pBullet.nuclear = sBullet.nuclear; + pBullet.beehive = sBullet.beehive; + pBullet.subMunitionType = BulletInfo.bullets[sBullet.subMunitionType]; + //pBullet.homing = BulletInfo.homing; + pBullet.impulse = sBullet.impulse; + pBullet.massMod = sBullet.massMod; + switch (sBullet.bulletDragTypeName) + { + case "None": + pBullet.dragType = PooledBullet.BulletDragTypes.None; + break; + case "AnalyticEstimate": + pBullet.dragType = PooledBullet.BulletDragTypes.AnalyticEstimate; + break; + case "NumericalIntegration": + pBullet.dragType = PooledBullet.BulletDragTypes.NumericalIntegration; + break; + default: + pBullet.dragType = PooledBullet.BulletDragTypes.AnalyticEstimate; + break; + } + pBullet.bullet = BulletInfo.bullets[sBullet.name]; + pBullet.stealResources = thief; + pBullet.dmgMult = dmgMult; + pBullet.isAPSprojectile = isAPSprojectile; + pBullet.tgtShell = tgtShell; + pBullet.tgtRocket = tgtRocket; + pBullet.gameObject.SetActive(true); + } + } + else + { + RocketInfo sRocket = RocketInfo.rockets[subMunitionType]; + for (int s = 0; s < sRocket.subProjectileCount; s++) + { + GameObject rocketObj = ModuleWeapon.rocketPool[sRocket.name].GetPooledObject(); + rocketObj.transform.position = transform.position; + //rocketObj.transform.rotation = currentRocketTfm.rotation; + rocketObj.transform.rotation = transform.rotation; + rocketObj.transform.localScale = this.transform.localScale; + PooledRocket rocket = rocketObj.GetComponent(); + rocket.explModelPath = explModelPath; + rocket.explSoundPath = explSoundPath; + rocket.caliber = sRocket.caliber; + rocket.apMod = sRocket.apMod; + rocket.rocketMass = sRocket.rocketMass; + rocket.blastRadius = blastRadius = BlastPhysicsUtils.CalculateBlastRange(sRocket.tntMass); + rocket.thrust = sRocket.thrust; + rocket.thrustTime = sRocket.thrustTime; + rocket.flak = sRocket.flak; + rocket.detonationRange = detonationRange; + rocket.maxAirDetonationRange = maxAirDetonationRange; + rocket.tntMass = sRocket.tntMass; + rocket.shaped = sRocket.shaped; + rocket.concussion = sRocket.impulse; + rocket.gravitic = sRocket.gravitic; + rocket.EMP = sRocket.EMP; + rocket.nuclear = sRocket.nuclear; + rocket.beehive = sRocket.beehive; + if (beehive) + { + rocket.subMunitionType = sRocket.subMunitionType; + } + rocket.choker = choker; + rocket.impulse = sRocket.force; + rocket.massMod = sRocket.massMod; + rocket.incendiary = sRocket.incendiary; + rocket.randomThrustDeviation = sRocket.thrustDeviation; + rocket.bulletDmgMult = bulletDmgMult; + rocket.sourceVessel = sourceVessel; + rocket.sourceWeapon = sourceWeapon; + rocketObj.transform.SetParent(transform); + rocket.rocketName = rocketName + " submunition"; + rocket.team = team; + rocket.parentRB = parentRB; + rocket.rocket = RocketInfo.rockets[sRocket.name]; + rocket.rocketSoundPath = rocketSoundPath; + rocket.thief = thief; //currently will only steal on direct hit + rocket.dmgMult = dmgMult; + if (isAPSprojectile) + { + rocket.isAPSprojectile = true; + rocket.tgtShell = tgtShell; + rocket.tgtRocket = tgtRocket; + } + rocketObj.SetActive(true); } } - gameObject.SetActive(false); } void SetupAudio() @@ -768,7 +1061,7 @@ void SetupAudio() audioSource.pitch = 1f; audioSource.priority = 255; audioSource.spatialBlend = 1; - audioSource.clip = GameDatabase.Instance.GetAudioClip(rocketSoundPath); + audioSource.clip = SoundUtils.GetAudioClip(rocketSoundPath); UpdateVolume(); BDArmorySetup.OnVolumeChange += UpdateVolume; @@ -787,7 +1080,7 @@ void OnGUI() { if (distanceFromStart > 100) { - BDGUIUtils.DrawTextureOnWorldPos(transform.position, BDTISetup.Instance.TextureIconRocket, new Vector2(20, 20), 0); + GUIUtils.DrawTextureOnWorldPos(transform.position, BDTISetup.Instance.TextureIconRocket, new Vector2(20, 20), 0); } } } diff --git a/BDArmory/Bullets/RocketInfo.cs b/BDArmory/Ammo/RocketInfo.cs similarity index 74% rename from BDArmory/Bullets/RocketInfo.cs rename to BDArmory/Ammo/RocketInfo.cs index 6b51b7075..0c98c4472 100644 --- a/BDArmory/Bullets/RocketInfo.cs +++ b/BDArmory/Ammo/RocketInfo.cs @@ -8,19 +8,27 @@ namespace BDArmory.Bullets public class RocketInfo { public string name { get; private set; } + public string DisplayName { get; private set; } public float rocketMass { get; private set; } public float caliber { get; private set; } + public float apMod { get; private set; } public float thrust { get; private set; } public float thrustTime { get; private set; } + public float lifeTime { get; private set; } = 10f; // Need this here for trajectory sim timing. Could make it a proper config value. public bool shaped { get; private set; } public bool flak { get; private set; } public bool EMP { get; private set; } public bool choker { get; private set; } public bool gravitic { get; private set; } public bool impulse { get; private set; } + public float massMod { get; private set; } + public float force { get; private set; } public bool explosive { get; private set; } public bool incendiary { get; private set; } public float tntMass { get; private set; } + public bool nuclear { get; private set; } + public bool beehive { get; private set; } + public string subMunitionType { get; private set; } public int subProjectileCount { get; private set; } public float thrustDeviation { get; private set; } public string rocketModelPath { get; private set; } @@ -29,12 +37,14 @@ public class RocketInfo public static HashSet rocketNames; public static RocketInfo defaultRocket; - public RocketInfo(string name, float rocketMass, float caliber, float thrust, float thrustTime, - bool shaped, bool flak, bool EMP, bool choker, bool gravitic, bool impulse, bool explosive, bool incendiary, float tntMass, int subProjectileCount, float thrustDeviation, string rocketModelPath) + public RocketInfo(string name, string DisplayName, float rocketMass, float caliber, float apMod, float thrust, float thrustTime, + bool shaped, bool flak, bool EMP, bool choker, bool gravitic, bool impulse, float massMod, float force, bool explosive, bool incendiary, float tntMass, bool nuclear, bool beehive, string subMunitionType, int subProjectileCount, float thrustDeviation, string rocketModelPath) { this.name = name; + this.DisplayName = DisplayName; this.rocketMass = rocketMass; this.caliber = caliber; + this.apMod = apMod; this.thrust = thrust; this.thrustTime = thrustTime; this.shaped = shaped; @@ -43,9 +53,14 @@ public RocketInfo(string name, float rocketMass, float caliber, float thrust, fl this.choker = choker; this.gravitic = gravitic; this.impulse = impulse; + this.massMod = massMod; + this.force = force; this.explosive = explosive; this.incendiary = incendiary; this.tntMass = tntMass; + this.nuclear = nuclear; + this.beehive = beehive; + this.subMunitionType = subMunitionType; this.subProjectileCount = subProjectileCount; this.thrustDeviation = thrustDeviation; this.rocketModelPath = rocketModelPath; @@ -69,8 +84,10 @@ public static void Load() Debug.Log("[BDArmory.RocketInfo]: Parsing default rocket definition from " + nodes[i].parent.name); defaultRocket = new RocketInfo( "def", + (string)ParseField(node, "DisplayName", typeof(string)), (float)ParseField(node, "rocketMass", typeof(float)), (float)ParseField(node, "caliber", typeof(float)), + (float)ParseField(node, "apMod", typeof(float)), (float)ParseField(node, "thrust", typeof(float)), (float)ParseField(node, "thrustTime", typeof(float)), (bool)ParseField(node, "shaped", typeof(bool)), @@ -79,9 +96,14 @@ public static void Load() (bool)ParseField(node, "choker", typeof(bool)), (bool)ParseField(node, "gravitic", typeof(bool)), (bool)ParseField(node, "impulse", typeof(bool)), + (float)ParseField(node, "massMod", typeof(float)), + (float)ParseField(node, "force", typeof(float)), (bool)ParseField(node, "explosive", typeof(bool)), (bool)ParseField(node, "incendiary", typeof(bool)), (float)ParseField(node, "tntMass", typeof(float)), + (bool)ParseField(node, "nuclear", typeof(bool)), + (bool)ParseField(node, "beehive", typeof(bool)), + (string)ParseField(node, "subMunitionType", typeof(string)), Math.Max((int)ParseField(node, "subProjectileCount", typeof(int)), 1), (float)ParseField(node, "thrustDeviation", typeof(float)), (string)ParseField(node, "rocketModelPath", typeof(string)) @@ -110,8 +132,10 @@ public static void Load() rockets.Add( new RocketInfo( name_, + (string)ParseField(node, "DisplayName", typeof(string)), (float)ParseField(node, "rocketMass", typeof(float)), (float)ParseField(node, "caliber", typeof(float)), + (float)ParseField(node, "apMod", typeof(float)), (float)ParseField(node, "thrust", typeof(float)), (float)ParseField(node, "thrustTime", typeof(float)), (bool)ParseField(node, "shaped", typeof(bool)), @@ -120,9 +144,14 @@ public static void Load() (bool)ParseField(node, "choker", typeof(bool)), (bool)ParseField(node, "gravitic", typeof(bool)), (bool)ParseField(node, "impulse", typeof(bool)), + (float)ParseField(node, "massMod", typeof(float)), + (float)ParseField(node, "force", typeof(float)), (bool)ParseField(node, "explosive", typeof(bool)), (bool)ParseField(node, "incendiary", typeof(bool)), (float)ParseField(node, "tntMass", typeof(float)), + (bool)ParseField(node, "nuclear", typeof(bool)), + (bool)ParseField(node, "beehive", typeof(bool)), + (string)ParseField(node, "subMunitionType", typeof(string)), (int)ParseField(node, "subProjectileCount", typeof(int)), (float)ParseField(node, "thrustDeviation", typeof(float)), (string)ParseField(node, "rocketModelPath", typeof(string)) @@ -165,8 +194,17 @@ private static object ParseField(ConfigNode node, string field, Type type) if (defaultRocket != null) { // Give a warning about the missing or invalid value, then use the default value using reflection to find the field. - var defaultValue = typeof(RocketInfo).GetProperty(field, BindingFlags.Public | BindingFlags.Instance).GetValue(defaultRocket); - Debug.LogError("[BDArmory.RocketInfo]: Using default value of " + defaultValue.ToString() + " for " + field + " | " + e.ToString()); + if (field == "DisplayName") return string.Empty; + var defaultValue = typeof(RocketInfo).GetProperty(field == "DisplayName" ? "name" : field, BindingFlags.Public | BindingFlags.Instance).GetValue(defaultRocket); + + if (field == "apMod" || field == "EMP" || field == "nuclear" || field == "beehive" || field == "subMunitionType" || field == "choker" || field == "gravitic" || field == "impulse" || field == "massMod" || field == "force") + { + //not having these throw an error message since these are all optional and default to false, prevents bullet defs from bloating like rockets did + } + else + { + Debug.LogError("[BDArmory.BulletInfo]: Using default value of " + defaultValue.ToString() + " for " + field + " | " + e.ToString()); + } return defaultValue; } else diff --git a/BDArmory/Bullets/_description b/BDArmory/Ammo/_description similarity index 100% rename from BDArmory/Bullets/_description rename to BDArmory/Ammo/_description diff --git a/BDArmory.Core/ArmorInfo.cs b/BDArmory/Armor/ArmorInfo.cs similarity index 61% rename from BDArmory.Core/ArmorInfo.cs rename to BDArmory/Armor/ArmorInfo.cs index d1fcbab75..e00087406 100644 --- a/BDArmory.Core/ArmorInfo.cs +++ b/BDArmory/Armor/ArmorInfo.cs @@ -1,12 +1,11 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Reflection; -using System.Text; -using System.Threading.Tasks; using UnityEngine; -namespace BDArmory.Core +using BDArmory.Utils; + +namespace BDArmory.Armor { public class ArmorInfo { @@ -14,28 +13,90 @@ public class ArmorInfo public float Density { get; private set; } //mass kg/m3 lighter is better. Or is it? public float Strength { get; private set; } //in MPa, yieldstrength for material, controls fail point for material when projectile can penetrate. Higher is better public float Hardness { get; private set; } //hardness, in MPa, of material. Controls how much deformation impacting projectiles experience + public float Yield { get; private set; } // Yield strength of material. Only needed while loading, but needs to be here for reflection if an armor definition is missing it. + public float YoungModulus { get; private set; } // Young's Modulus of material. Only needed while loading, but needs to be here for reflection if an armor definition is missing it. public float Ductility { get; private set; } //measure of ductility, 0 is hardened ceramic, 100 is rubber. Mild steel is about 15. ideally should be around 15-25. //Too low, and armor is brittle. Too High, and armor cannot effectively stop projectiles in reasonable distance public float Diffusivity { get; private set; } //ability to disperse electrical/thermal energy when material is subject to laser/EMP attack. Higher is better public float SafeUseTemp { get; private set; } //In Kelvin, determines max temp armor retains full mechanical properties public float Cost { get; private set; } + public float vFactor { get; private set; } + + + public float muParam1 { get; private set; } + public float muParam2 { get; private set; } + public float muParam3 { get; private set; } + public float muParam1S { get; private set; } + public float muParam2S { get; private set; } + public float muParam3S { get; private set; } + public float HEEquiv { get; private set; } + public float HEATEquiv { get; private set; } + + //public bool Reactive {get; private set; } have a reactive armor bool? public static ArmorInfos armors; public static List armorNames; public static ArmorInfo defaultArmor; - public ArmorInfo(string name, float Density, float Strength, float Hardness, float Ductility, float Diffusivity, float SafeUseTemp, float Cost) + public ArmorInfo(string name, float Density, float Strength, float Hardness, float yield, float youngModulus, float Ductility, float Diffusivity, float SafeUseTemp, float Cost, float defaultPenShrapnel, float defaultPenHEAT) { this.name = name; this.Density = Density; this.Strength = Strength; this.Hardness = Hardness; + this.Yield = yield; + this.YoungModulus = youngModulus; this.Ductility = Ductility; this.Diffusivity = Diffusivity; this.SafeUseTemp = SafeUseTemp; this.Cost = Cost; + + // Since we don't actually need yield and youngModulus we'll just calculate + // vFactor and discard those two. vFactor is simply the density of the armor + // divided by two times the resistance of the armor H, calculated using Tate's + // formula, found either in his 1986 paper or in the publically available US + // Army technical memorandum "TERMINAL BALLISTICS TEST AND ANALYSIS + // GUIDELINES FOR THE PENETRATION MECHANICS BRANCH" (ADA246922) on page 104 + // of the PDF, listed as Advancing Cavity (Tate 1986a)", this is used + // throughout the equations proposed by Frank and Zook as special case + // solutions to the model proposed by Tate and Alekseevskii + this.vFactor = Density / (2.0f * (yield * (2.0f / 3.0f + Mathf.Log((2.0f * + youngModulus * 1000f) / (3.0f * yield))) * 1000000.0f)); + + // mu is the sqrt of the ratio of armor density to projectile density + // We don't actually need mu itself or the following variants of it, just + // the muParams so we'll calculate those instead. + float muSquared = Density / (11340.0f); + float mu = BDAMath.Sqrt(muSquared); + float muInverse = 1.0f / mu; + float muInverseSquared = 1.0f / muSquared; + + // These parameters are all used in the equations proposed by Frank and Zook + // in their 1987 paper "ENERGY-EFFICIENT PENETRATION AND PERFORATION OF + // TARGETS IN THE HYPERVELOCITY REGIME". These are all based on the constant + // mu, explained above. We are pre-calculating these terms in the function in + // order to optimize the performance of the equation + this.muParam1 = muInverse / (1.0f + mu); + this.muParam2 = muInverse; + this.muParam3 = (muInverseSquared + 1.0f / 3.0f); + + // Doing the same thing as above but with the sabot density instead. Note that + // if we ever think about having custom round density's then we're going to + // have to build a dictionary instead using all available armor types and + // projectiles so as to maintain performance as proposed by DocNappers + muSquared = Density / (19000.0f); + mu = BDAMath.Sqrt(muSquared); + muInverse = 1.0f / mu; + muInverseSquared = 1.0f / muSquared; + + this.muParam1S = muInverse / (1.0f + mu); + this.muParam2S = muInverse; + this.muParam3S = (muInverseSquared + 1.0f / 3.0f); + + this.HEEquiv = defaultPenShrapnel / ProjectileUtils.CalculatePenetration(15, 430, 0.02f, 1, Strength, this.vFactor, this.muParam1, this.muParam2, this.muParam3); + this.HEATEquiv = defaultPenHEAT / ProjectileUtils.CalculatePenetration(6, 5000, 0.13098f, 1, Strength, this.vFactor, this.muParam1, this.muParam2, this.muParam3); } public static void Load() @@ -46,6 +107,11 @@ public static void Load() UrlDir.UrlConfig[] nodes = GameDatabase.Instance.GetConfigs("ARMOR"); ConfigNode node; + // Based on average piece of shrapnel + float defaultPenShrapnel = ProjectileUtils.CalculatePenetration(15, 430, 0.02f, 1); + // Based on 120x570 mm NATO HEAT shell + float defaultPenHEAT = ProjectileUtils.CalculatePenetration(6, 5000, 0.13098f, 1); + // First locate BDA's default armor definition so we can fill in missing fields. if (defaultArmor == null) for (int i = 0; i < nodes.Length; ++i) @@ -59,10 +125,14 @@ public static void Load() (float)ParseField(node, "Density", typeof(float)), (float)ParseField(node, "Strength", typeof(float)), (float)ParseField(node, "Hardness", typeof(float)), + (float)ParseField(node, "Yield", typeof(float)), + (float)ParseField(node, "YoungModulus", typeof(float)), (float)ParseField(node, "Ductility", typeof(float)), (float)ParseField(node, "Diffusivity", typeof(float)), (float)ParseField(node, "SafeUseTemp", typeof(float)), - (float)ParseField(node, "Cost", typeof(float)) + (float)ParseField(node, "Cost", typeof(float)), + defaultPenShrapnel, + defaultPenHEAT ); armors.Add(defaultArmor); armorNames.Add("def"); @@ -91,10 +161,14 @@ public static void Load() (float)ParseField(node, "Density", typeof(float)), (float)ParseField(node, "Strength", typeof(float)), (float)ParseField(node, "Hardness", typeof(float)), + (float)ParseField(node, "Yield", typeof(float)), + (float)ParseField(node, "YoungModulus", typeof(float)), (float)ParseField(node, "Ductility", typeof(float)), (float)ParseField(node, "Diffusivity", typeof(float)), (float)ParseField(node, "SafeUseTemp", typeof(float)), - (float)ParseField(node, "Cost", typeof(float)) + (float)ParseField(node, "Cost", typeof(float)), + defaultPenShrapnel, + defaultPenHEAT ) ); armorNames.Add(name_); diff --git a/BDArmory/Armor/BDAdjustableArmor.cs b/BDArmory/Armor/BDAdjustableArmor.cs new file mode 100644 index 000000000..7bc42e5c7 --- /dev/null +++ b/BDArmory/Armor/BDAdjustableArmor.cs @@ -0,0 +1,447 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +using BDArmory.Damage; +using KSP.Localization; +using BDArmory.Utils; + +namespace BDArmory.Armor +{ + public class BDAdjustableArmor : PartModule + { + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_ArmorAdjustParts"),//Move Child Parts + UI_Toggle(disabledText = "#LOC_BDArmory_false", enabledText = "#LOC_BDArmory_true")]//false--true + public bool moveChildParts = true; + + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_ArmorWidth"),//Armor Width + UI_FloatSemiLogRange(minValue = 0.1f, maxValue = 16, scene = UI_Scene.Editor)] + public float Width = 1; + + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = false, guiName = "#LOC_BDArmory_ArmorWidthR"),//Right Side Width + UI_FloatSemiLogRange(minValue = 0.1f, maxValue = 8, scene = UI_Scene.Editor)] + public float scaleneWidth = 1; + + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_ArmorLength"),//Armor Length + UI_FloatSemiLogRange(minValue = 0.1f, maxValue = 16, scene = UI_Scene.Editor)] + public float Length = 1; + + [KSPField] + public float maxScale = 16; + + [KSPEvent(guiActive = false, guiActiveEditor = false, guiName = "#LOC_BDArmory_ArmorTriIso", active = true)]//Toggle Tri Type + public void ToggleTriTypeOption() => ToggleTriTypeOptionHandler(); + void ToggleTriTypeOptionHandler(bool applySym = true, Toggle state = Toggle.Toggle) + { + switch (state) + { + case Toggle.Toggle: + scaleneTri = !scaleneTri; + break; + case Toggle.NoChange: + break; + case Toggle.Off: + scaleneTri = false; + break; + case Toggle.On: + scaleneTri = true; + break; + } + + Fields["scaleneWidth"].guiActiveEditor = scaleneTri; + UI_FloatSemiLogRange AWidth = (UI_FloatSemiLogRange)Fields["Width"].uiControlEditor; + AWidth.UpdateLimits(clamped ? 0.1f : 0.01f, scaleneTri ? clamped ? maxScale / 2 : 50 : clamped ? maxScale : 100); + + if (scaleneTri) + { + Fields["Width"].guiName = StringUtils.Localize("#LOC_BDArmory_ArmorWidthL"); + Events["ToggleTriTypeOption"].guiName = StringUtils.Localize("#LOC_BDArmory_ArmorTriSca"); + } + else + { + Fields["Width"].guiName = StringUtils.Localize("#LOC_BDArmory_ArmorWidth"); + Events["ToggleTriTypeOption"].guiName = StringUtils.Localize("#LOC_BDArmory_ArmorTriIso"); + } + GUIUtils.RefreshAssociatedWindows(part); + if (applySym) + { + using (List.Enumerator sym = part.symmetryCounterparts.GetEnumerator()) + while (sym.MoveNext()) + { + if (sym.Current == null) continue; + sym.Current.FindModuleImplementing().ToggleTriTypeOptionHandler(false, state); + } + } + } + + [KSPEvent(active = true, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_UnclampTuning_disabledText")]//Toggle scale limit + public void ToggleScaleClamp() => ToggleScaleClampHandler(); + public void ToggleScaleClampHandler(bool applySym = true, Toggle state = Toggle.Toggle) + { + switch (state) + { + case Toggle.Toggle: + clamped = !clamped; + break; + case Toggle.NoChange: + break; + case Toggle.Off: + clamped = false; + break; + case Toggle.On: + clamped = true; + break; + } + + UI_FloatSemiLogRange AWidth = (UI_FloatSemiLogRange)Fields["Width"].uiControlEditor; + AWidth.UpdateLimits(clamped ? 0.1f : 0.01f, scaleneTri ? clamped ? maxScale / 2 : 50 : clamped ? maxScale : 100); + UI_FloatSemiLogRange ALength = (UI_FloatSemiLogRange)Fields["Length"].uiControlEditor; + ALength.UpdateLimits(clamped ? 0.1f : 0.01f, clamped ? maxScale : 100); + UI_FloatSemiLogRange SWidth = (UI_FloatSemiLogRange)Fields["scaleneWidth"].uiControlEditor; + SWidth.UpdateLimits(clamped ? 0.1f : 0.01f, clamped ? maxScale / 2 : 50); + + if (!clamped) + { + Events["ToggleScaleClamp"].guiName = StringUtils.Localize("#LOC_BDArmory_UnclampTuning_enabledText"); + } + else + { + Events["ToggleScaleClamp"].guiName = StringUtils.Localize("#LOC_BDArmory_UnclampTuning_disabledText"); + } + GUIUtils.RefreshAssociatedWindows(part); + if (applySym) + { + using (List.Enumerator sym = part.symmetryCounterparts.GetEnumerator()) + while (sym.MoveNext()) + { + if (sym.Current == null) continue; + sym.Current.FindModuleImplementing().ToggleScaleClampHandler(false, state); + } + } + } + + [KSPField(isPersistant = true)] + bool clamped = true; + + //public bool isCurvedPanel = false; + private float armorthickness = 1; + private float Oldthickness = 1; + + [KSPField] + public bool isTriangularPanel = false; + + [KSPField(isPersistant = true)] + bool scaleneTri = false; + + [KSPField] + public string TriangleType = "none"; + + [KSPField] + public string ArmorTransformName = "ArmorTransform"; //transform of armor panel mesh/box collider + Transform[] armorTransforms; + + [KSPField] + public string ScaleneTransformName = "ScaleneTransform"; //transform of armor panel mesh/box collider + Transform[] scaleneTransforms; + + [KSPField] + public string ThicknessTransformName = "ThicknessTransform"; //name of armature to control thickness of curved panels + Transform ThicknessTransform; + + [KSPField] public string stackNodePosition; + + Dictionary originalStackNodePosition; + + HitpointTracker armor; + + public override void OnStart(StartState state) + { + armorTransforms = part.FindModelTransforms(ArmorTransformName); + ThicknessTransform = part.FindModelTransform(ThicknessTransformName); + if (isTriangularPanel && TriangleType != "Right") + { + Events["ToggleTriTypeOption"].guiActiveEditor = true; + scaleneTransforms = part.FindModelTransforms(ScaleneTransformName); + UI_FloatSemiLogRange SWidth = (UI_FloatSemiLogRange)Fields["scaleneWidth"].uiControlEditor; + SWidth.onFieldChanged = AdjustSWidth; + // SWidth.UpdateLimits(0.1f, maxScale / 2); + } + if (HighLogic.LoadedSceneIsEditor) + { + ParseStackNodePosition(); + StartCoroutine(DelayedUpdateStackNode()); + GameEvents.onEditorShipModified.Add(OnEditorShipModifiedEvent); + ToggleTriTypeOptionHandler(state: Toggle.NoChange); // Initialise the UI for the Triangle Type toggle + ToggleScaleClampHandler(state: Toggle.NoChange); // Initialise the UI for the Clamped toggle + } + UpdateThickness(true); + UI_FloatSemiLogRange AWidth = (UI_FloatSemiLogRange)Fields["Width"].uiControlEditor; + AWidth.onFieldChanged = AdjustWidth; + // AWidth.UpdateLimits(0.1f, maxScale); + UI_FloatSemiLogRange ALength = (UI_FloatSemiLogRange)Fields["Length"].uiControlEditor; + ALength.onFieldChanged = AdjustLength; + // ALength.UpdateLimits(0.1f, maxScale); + + armor = GetComponent(); + UpdateScale(Width, Length, scaleneWidth, false); + GUIUtils.RefreshAssociatedWindows(part); + } + void ParseStackNodePosition() + { + originalStackNodePosition = new Dictionary(); + string[] nodes = stackNodePosition.Split(new char[] { ';' }); + for (int i = 0; i < nodes.Length; i++) + { + string[] split = nodes[i].Split(new char[] { ',' }); + string id = split[0]; + Vector3 position = new Vector3(float.Parse(split[1]), float.Parse(split[2]), float.Parse(split[3])); + originalStackNodePosition.Add(id, position); + } + } + + IEnumerator DelayedUpdateStackNode() + { + yield return null; + UpdateStackNode(false); + } + + private void OnDestroy() + { + GameEvents.onEditorShipModified.Remove(OnEditorShipModifiedEvent); + } + + public void AdjustWidth(BaseField field, object obj) + { + Width = Mathf.Clamp(Width, clamped ? 0.1f : 0.01f, scaleneTri ? clamped ? maxScale / 2 : 50 : clamped ? maxScale : 100); + for (int i = 0; i < armorTransforms.Length; i++) + { + armorTransforms[i].localScale = new Vector3(Width, Length, armorthickness); + } + if (isTriangularPanel && TriangleType != "Right" && !scaleneTri) + { + for (int i = 0; i < scaleneTransforms.Length; i++) + { + scaleneTransforms[i].localScale = new Vector3(Width, Length, armorthickness); + } + } + using (List.Enumerator sym = part.symmetryCounterparts.GetEnumerator()) + while (sym.MoveNext()) + { + if (sym.Current == null) continue; + sym.Current.FindModuleImplementing().UpdateScale(Width, Length, scaleneWidth); //needs to be changed to use updatewitth() - FIXME later, future SI + } + updateArmorStats(); + UpdateStackNode(true); + } + public void AdjustSWidth(BaseField field, object obj) + { + scaleneWidth = Mathf.Clamp(scaleneWidth, clamped ? 0.1f : 0.01f, clamped ? maxScale / 2 : 50); + for (int i = 0; i < scaleneTransforms.Length; i++) + { + scaleneTransforms[i].localScale = new Vector3(scaleneWidth * 2, Length, armorthickness); + } + using (List.Enumerator sym = part.symmetryCounterparts.GetEnumerator()) + while (sym.MoveNext()) + { + if (sym.Current == null) continue; + sym.Current.FindModuleImplementing().UpdateScale(Width, Length, scaleneWidth); //needs to be changed to use updatewitth() - FIXME later, future SI + } + updateArmorStats(); + UpdateStackNode(true); + } + public void AdjustLength(BaseField field, object obj) + { + Length = Mathf.Clamp(Length, clamped ? 0.1f : 0.01f, clamped ? maxScale : 100); + for (int i = 0; i < armorTransforms.Length; i++) + { + armorTransforms[i].localScale = new Vector3(Width, Length, armorthickness); + } + if (isTriangularPanel && TriangleType != "Right") + { + for (int i = 0; i < scaleneTransforms.Length; i++) + { + scaleneTransforms[i].localScale = new Vector3(scaleneWidth, Length, armorthickness); + } + } + using (List.Enumerator sym = part.symmetryCounterparts.GetEnumerator()) + while (sym.MoveNext()) + { + if (sym.Current == null) continue; + sym.Current.FindModuleImplementing().UpdateScale(Width, Length, scaleneWidth); + } + updateArmorStats(); + UpdateStackNode(true); + } + + public void UpdateScale(float width, float length, float scalenewidth = 1, bool updateNodes = true) + { + Width = width; + scaleneWidth = scalenewidth; + Length = length; + + for (int i = 0; i < armorTransforms.Length; i++) + { + armorTransforms[i].localScale = new Vector3(Width, Length, Mathf.Clamp((armor.Armor / 10), 0.1f, 1500)); + } + if (isTriangularPanel && TriangleType != "Right") + { + for (int i = 0; i < scaleneTransforms.Length; i++) + { + scaleneTransforms[i].localScale = new Vector3(scaleneTri ? scaleneWidth : Width, Length, Mathf.Clamp((armor.Armor / 10), 0.1f, 1500)); + } + } + updateArmorStats(); + if (updateNodes) UpdateStackNode(true); + } + IEnumerator updateDrag() + { + yield return null; + DragCube DragCube = DragCubeSystem.Instance.RenderProceduralDragCube(part); + part.DragCubes.ClearCubes(); + part.DragCubes.Cubes.Add(DragCube); + part.DragCubes.ResetCubeWeights(); + part.DragCubes.ForceUpdate(true, true, false); + part.DragCubes.SetDragWeights(); + if (HighLogic.LoadedSceneIsEditor) GameEvents.onEditorShipModified.Fire(EditorLogic.fetch.ship); + } + public void UpdateStackNode(bool translateChidren) + { + using (List.Enumerator stackNode = part.attachNodes.GetEnumerator()) + while (stackNode.MoveNext()) + { + if (stackNode.Current?.nodeType != AttachNode.NodeType.Stack || + !originalStackNodePosition.ContainsKey(stackNode.Current.id)) continue; + + if (stackNode.Current.id == "top" || stackNode.Current.id == "bottom") + { + Vector3 prevPos = stackNode.Current.position; + Vector3 prevAngle = stackNode.Current.orientation; + int offsetScale = 2; + if (isTriangularPanel && TriangleType != "Right" && !scaleneTri) + { + offsetScale = 4; + } + if (stackNode.Current.id == "top") + { + stackNode.Current.size = Mathf.CeilToInt(Width / 2); + stackNode.Current.breakingForce = Width * 100; + stackNode.Current.breakingTorque = Width * 100; + stackNode.Current.position.x = originalStackNodePosition[stackNode.Current.id].x + (((Width - 1) / (scaleneTri ? 2 : 1)) / offsetScale); //if eqi tri this needs to be /4 + if (isTriangularPanel) stackNode.Current.orientation = new Vector3(1, 0, -((Width / 2) / Length)); + if (translateChidren) MoveParts(stackNode.Current, stackNode.Current.position - prevPos, stackNode.Current.orientation - prevAngle); + } + else + { + stackNode.Current.size = Mathf.CeilToInt(scaleneTri ? scaleneWidth / 2 : Width / 2); + stackNode.Current.breakingForce = scaleneTri ? scaleneWidth : Width * 100; + stackNode.Current.breakingTorque = scaleneTri ? scaleneWidth : Width * 100; + stackNode.Current.position.x = originalStackNodePosition[stackNode.Current.id].x - ((scaleneTri ? ((scaleneWidth - 1) / 2) : Width - 1) / offsetScale);// and a right tri hypotenuse node shouldn't move at all + if (isTriangularPanel && TriangleType != "Right") + { + stackNode.Current.orientation = new Vector3(-1, 0, -(((scaleneTri ? scaleneWidth : Width) / 2) / Length)); + } + if (translateChidren) MoveParts(stackNode.Current, stackNode.Current.position - prevPos, stackNode.Current.orientation - prevAngle); //look into making triangle side nodes rotate attachnode based on new angle? AttachNode.Orientation? + } + } + else if (stackNode.Current.id == "left" || stackNode.Current.id == "right") + { + stackNode.Current.size = Mathf.CeilToInt(Length / 2); + stackNode.Current.breakingForce = Length * 100; + stackNode.Current.breakingTorque = Length * 100; + Vector3 prevPos = stackNode.Current.position; + if (stackNode.Current.id == "right") + { + stackNode.Current.position.z = originalStackNodePosition[stackNode.Current.id].z + ((Length - 1) / 2); + if (translateChidren) MoveParts(stackNode.Current, stackNode.Current.position - prevPos, Vector3.zero); + } + else + { + stackNode.Current.position.z = originalStackNodePosition[stackNode.Current.id].z - ((Length - 1) / 2); + if (translateChidren) MoveParts(stackNode.Current, stackNode.Current.position - prevPos, Vector3.zero); + } + } + else if (stackNode.Current.id == "side") + { + stackNode.Current.size = Mathf.CeilToInt(((Width / 2) + (Length / 2)) / 2); + stackNode.Current.orientation = new Vector3(1, 0, -(Width / Length)); + } + } + } + public void MoveParts(AttachNode node, Vector3 delta, Vector3 angleDelta) + { + if (!moveChildParts) return; + if (node.attachedPart is Part pushTarget) + { + if (pushTarget == null) return; + Vector3 worldDelta = part.transform.TransformVector(delta); + pushTarget.transform.position += worldDelta; + //Vector3 worldAngle = part.transform.TransformVector(angleDelta); + //pushTarget.transform.rotation += worldAngle; + } + } + public void updateArmorStats() + { + armor.armorVolume = ((scaleneTri ? scaleneWidth + Width : Width) * Length); + if (isTriangularPanel) + { + armor.armorVolume /= 2; + } + armor.ArmorSetup(null, null); + StartCoroutine(updateDrag()); + } + void UpdateThickness(bool onLoad = false) + { + if (armor != null && armorTransforms != null) + { + armorthickness = Mathf.Clamp((armor.Armor / 10), 0.1f, 1500); + + if (armorthickness != Oldthickness) + { + for (int i = 0; i < armorTransforms.Length; i++) + { + armorTransforms[i].localScale = new Vector3(Width, Length, armorthickness); + } + if (isTriangularPanel && TriangleType != "Right") + { + for (int i = 0; i < scaleneTransforms.Length; i++) + { + scaleneTransforms[i].localScale = new Vector3(scaleneTri ? scaleneWidth : Width, Length, armorthickness); + } + } + } + } + else + { + //if (armor == null) Debug.Log("[BDAAdjustableArmor] No HitpointTracker found! aborting UpdateThickness()!"); + //if (armorTransforms == null) Debug.Log("[BDAAdjustableArmor] No ArmorTransform found! aborting UpdateThickness()!"); + return; + } + if (onLoad) return; //don't adjust part placement on load + /* + if (armorthickness != Oldthickness) + { + float ratio = (armorthickness - Oldthickness) / 100; + + Vector3 prevPos = new Vector3(0f, Oldthickness / 100, 0f); + Vector3 delta = new Vector3(0f, armorthickness / 100, 0f); + Vector3 worldDelta = part.transform.TransformVector(delta); + List.Enumerator p = part.children.GetEnumerator(); + while (p.MoveNext()) + { + if (p.Current == null) continue; + if (p.Current.FindAttachNodeByPart(part) is AttachNode node && node.nodeType == AttachNode.NodeType.Surface) + { + + p.Current.transform.position += worldDelta; + } + } + Oldthickness = armorthickness; + } + */ + } + private void OnEditorShipModifiedEvent(ShipConstruct data) + { + UpdateThickness(); + } + } +} \ No newline at end of file diff --git a/BDArmory/Armor/HullInfo.cs b/BDArmory/Armor/HullInfo.cs new file mode 100644 index 000000000..4b8586a72 --- /dev/null +++ b/BDArmory/Armor/HullInfo.cs @@ -0,0 +1,153 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using UnityEngine; + +using BDArmory.Utils; + +namespace BDArmory.Armor +{ + public class HullInfo + { + public string name { get; private set; } //internal name + public string localizedName { get; private set; } //display name + public float massMod { get; private set; } //mass modifier + public float costMod { get; private set; } //cost modifier + public float healthMod { get; private set; } //health modifier + public float ignitionTemp { get; private set; } //can material catch fire? + public float maxTemp { get; private set; } //In Kelvin, determines max temp material can sustain before part is destroyed + public float ImpactMod { get; private set; } //impact tolerance modifier + + public static HullInfos materials; + public static List materialNames; + public static HullInfo defaultMaterial; + + public HullInfo(string name, string localizedName, float massMod, float costMod, float healthMod, float ignitionTemp, float maxTemp, float ImpactMod) + { + this.name = name; + this.localizedName = localizedName; + this.massMod = massMod; + this.costMod = costMod; + this.healthMod = healthMod; + this.ignitionTemp = ignitionTemp; + this.maxTemp = maxTemp; + this.ImpactMod = ImpactMod; + } + + public static void Load() + { + if (materials != null) return; // Only load the armor defs once on startup. + materials = new HullInfos(); + if (materialNames == null) materialNames = new List(); + UrlDir.UrlConfig[] nodes = GameDatabase.Instance.GetConfigs("MATERIAL"); + ConfigNode node; + + // First locate BDA's default armor definition so we can fill in missing fields. + if (defaultMaterial == null) + for (int i = 0; i < nodes.Length; ++i) + { + if (nodes[i].parent.name != "BD_Materials") continue; // Ignore other config files. + node = nodes[i].config; + if (!node.HasValue("name") || (string)ParseField(nodes[i].config, "name", typeof(string)) != "def") continue; // Ignore other configs. + Debug.Log("[BDArmory.MaterialInfo]: Parsing default material definition from " + nodes[i].parent.name); + defaultMaterial = new HullInfo( + "def", + (string)ParseField(node, "localizedName", typeof(string)), + (float)ParseField(node, "massMod", typeof(float)), + (float)ParseField(node, "costMod", typeof(float)), + (float)ParseField(node, "healthMod", typeof(float)), + (float)ParseField(node, "ignitionTemp", typeof(float)), + (float)ParseField(node, "maxTemp", typeof(float)), + 1 //(float)ParseField(node, "ImpactMod", typeof(float)) + ); + materials.Add(defaultMaterial); + materialNames.Add("def"); + break; + } + if (defaultMaterial == null) throw new ArgumentException("Failed to find BDArmory's default material definition.", "defaultMaterial"); + + // Now add in the rest of the materials. + for (int i = 0; i < nodes.Length; i++) + { + string name_ = ""; + try + { + node = nodes[i].config; + name_ = (string)ParseField(node, "name", typeof(string)); + if (materialNames.Contains(name_)) // Avoid duplicates. + { + if (nodes[i].parent.name != "BD_Materials" || name_ != "def") // Don't report the default bullet definition as a duplicate. + Debug.LogError("[BDArmory.MaterialInfo]: Material definition " + name_ + " from " + nodes[i].parent.name + " already exists, skipping."); + continue; + } + Debug.Log("[BDArmory.MaterialInfo]: Parsing definition of material " + name_ + " from " + nodes[i].parent.name); + materials.Add( + new HullInfo( + name_, + (string)ParseField(node, "localizedName", typeof(string)), + (float)ParseField(node, "massMod", typeof(float)), + (float)ParseField(node, "costMod", typeof(float)), + (float)ParseField(node, "healthMod", typeof(float)), + (float)ParseField(node, "ignitionTemp", typeof(float)), + (float)ParseField(node, "maxTemp", typeof(float)), + (float)ParseField(node, "ImpactMod", typeof(float)) + ) + ); + materialNames.Add(name_); + } + catch (Exception e) + { + Debug.LogError("[BDArmory.aterialInfo]: Error Loading Material Config '" + name_ + "' | " + e.ToString()); + } + } + //once armors are loaded, remove the def armor so it isn't found in later list parsings by HitpointTracker when updating parts armor + materials.Remove(defaultMaterial); + materialNames.Remove("def"); + } + + private static object ParseField(ConfigNode node, string field, Type type) + { + try + { + if (!node.HasValue(field)) + throw new ArgumentNullException(field, "Field '" + field + "' is missing."); + var value = node.GetValue(field); + try + { + if (type == typeof(string)) + { return value; } + else if (type == typeof(bool)) + { return bool.Parse(value); } + else if (type == typeof(int)) + { return int.Parse(value); } + else if (type == typeof(float)) + { return float.Parse(value); } + else + { throw new ArgumentException("Invalid type specified."); } + } + catch (Exception e) + { throw new ArgumentException("Field '" + field + "': '" + value + "' could not be parsed as '" + type.ToString() + "' | " + e.ToString(), field); } + } + catch (Exception e) + { + if (defaultMaterial != null) + { + // Give a warning about the missing or invalid value, then use the default value using reflection to find the field. + var defaultValue = typeof(HullInfo).GetProperty(field, BindingFlags.Public | BindingFlags.Instance).GetValue(defaultMaterial); + Debug.LogError("[BDArmory.MaterialInfo]: Using default value of " + defaultValue.ToString() + " for " + field + " | " + e.ToString()); + return defaultValue; + } + else + throw; + } + } + } + + public class HullInfos : List + { + public HullInfo this[string name] + { + get { return Find((value) => { return value.name == name; }); } + } + } +} \ No newline at end of file diff --git a/BDArmory/Modules/ModuleReactiveArmor.cs b/BDArmory/Armor/ModuleReactiveArmor.cs similarity index 90% rename from BDArmory/Modules/ModuleReactiveArmor.cs rename to BDArmory/Armor/ModuleReactiveArmor.cs index 1f8841dc7..fa1ba7a02 100644 --- a/BDArmory/Modules/ModuleReactiveArmor.cs +++ b/BDArmory/Armor/ModuleReactiveArmor.cs @@ -1,13 +1,11 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using BDArmory.Core; -using BDArmory.Core.Extension; -using BDArmory.Core.Module; +using UnityEngine; + +using BDArmory.Damage; +using BDArmory.Extensions; using BDArmory.FX; -using UnityEngine; +using BDArmory.Settings; -namespace BDArmory.Modules +namespace BDArmory.Armor { public class ModuleReactiveArmor : PartModule { @@ -76,7 +74,7 @@ public void UpdateSectionScales() direction = -sections[sectionsRemaining-1].up; ExplosionFx.CreateExplosion(sections[sectionsRemaining - 1].transform.position, 1, ExploModelPath, explSoundPath, ExplosionSourceType.BattleDamage, 30, part, SourceVessel, armorName, direction, 30, true); - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[ReactiveArmor] removing section, " + sectionsRemaining + " sections left"); + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log("[BDArmory.ReactiveArmor]: Removing section, " + sectionsRemaining + " sections left"); sectionsRemaining--; if (sectionsRemaining < 1) { diff --git a/BDArmory/BDArmory.csproj b/BDArmory/BDArmory.csproj index d2044b74a..203326c12 100644 --- a/BDArmory/BDArmory.csproj +++ b/BDArmory/BDArmory.csproj @@ -41,7 +41,7 @@ AnyCPU prompt - default + preview false @@ -51,7 +51,7 @@ true false - default + preview false @@ -127,13 +127,19 @@ - - - - + + + + + + + + + + - + @@ -142,29 +148,32 @@ - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + @@ -177,6 +186,12 @@ + + + + + + @@ -189,109 +204,66 @@ + - - - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + - + - - - - - - - @@ -300,12 +272,77 @@ - - - - {A6F1753E-9570-4C40-AF72-A179890582E5} - BDArmory.Core - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -323,6 +360,8 @@ + + @@ -333,7 +372,7 @@ - + @@ -354,10 +393,12 @@ + + @@ -406,43 +447,49 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + @@ -485,6 +532,8 @@ + + @@ -495,11 +544,13 @@ - - - + + + + + @@ -511,8 +562,8 @@ - - + + @@ -576,7 +627,7 @@ - + @@ -664,6 +715,8 @@ + + @@ -712,6 +765,7 @@ + @@ -721,6 +775,7 @@ + @@ -816,6 +871,7 @@ @echo Copying assemblies to Distribution $(Targetname) files... if not exist "$(ProjectDir)Distribution\GameData\%25ModName%25\Plugins\" mkdir "$(ProjectDir)Distribution\GameData\%25ModName%25\Plugins\" + del "$(ProjectDir)Distribution/GameData/${ModName}/Plugins/"BDArmory* xcopy /E /Y "$(TargetDir)"BDArmory*.dll "$(ProjectDir)Distribution\GameData\%25ModName%25\Plugins\" if $(ConfigurationName) == Debug ( @@ -840,6 +896,7 @@ export ModName=BDArmory echo Copying $(ConfigurationName) assemblies to Distribution $(Targetname) files... mkdir -p "$(ProjectDir)/Distribution/GameData/${ModName}/Plugins/" + rm "$(ProjectDir)Distribution/GameData/${ModName}/Plugins/"BDArmory* cp -a "$(TargetDir)"BDArmory*.dll "$(ProjectDir)Distribution/GameData/${ModName}/Plugins/" if [ "$(ConfigurationName)" = "Debug" ] then @@ -855,10 +912,13 @@ echo packaging new build... 7za a -tzip -r "$(ProjectDir)Distribution/${ModName}.@(VersionNumber)_`date -u -Iseconds`.zip" "$(ProjectDir)Distribution/*.*" - export KSP_DIR="`cat $(ProjectDir)../../_LocalDev/ksp_dir.txt`" - echo Deploy $(ProjectDir) Distribution files to test env: "${KSP_DIR}/GameData"... - echo copying:"$(ProjectDir)Distribution/GameData" to "${KSP_DIR}/GameData" - cp -a "$(ProjectDir)Distribution/GameData/${ModName}" "${KSP_DIR}/GameData" + + bash -c 'cat $(ProjectDir)../../_LocalDev/ksp_dir.txt | while read KSP_DIR; do + if [[ "${KSP_DIR:0:1}" == "#" ]]; then continue; fi + echo Deploy $(ProjectDir) Distribution files to test env: "${KSP_DIR}/GameData"... + echo copying:"$(ProjectDir)Distribution/GameData" to "${KSP_DIR}/GameData" + cp -a "$(ProjectDir)Distribution/GameData/${ModName}" "${KSP_DIR}/GameData" + done' echo Build/deploy complete! @@ -875,7 +935,7 @@ - + diff --git a/BDArmory/BDArmory.sln b/BDArmory/BDArmory.sln index 184a74b61..c35819a57 100644 --- a/BDArmory/BDArmory.sln +++ b/BDArmory/BDArmory.sln @@ -5,8 +5,6 @@ VisualStudioVersion = 16.0.30011.22 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BDArmory", "BDArmory.csproj", "{D86F2003-1724-4F4C-BB5A-B0109CB16F35}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BDArmory.Core", "..\BDArmory.Core\BDArmory.Core.csproj", "{A6F1753E-9570-4C40-AF72-A179890582E5}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/BDArmory/Competition/BDACompetitionMode.cs b/BDArmory/Competition/BDACompetitionMode.cs index 188ae0a15..6d28ce6e2 100644 --- a/BDArmory/Competition/BDACompetitionMode.cs +++ b/BDArmory/Competition/BDACompetitionMode.cs @@ -1,951 +1,26 @@ using System; using System.Collections; using System.Collections.Generic; -using System.IO; using System.Linq; -using BDArmory.Competition.RemoteOrchestration; -using BDArmory.Competition.VesselSpawning; +using UnityEngine; +using KSP.Localization; + using BDArmory.Control; -using BDArmory.Core.Extension; -using BDArmory.Core.Module; -using BDArmory.Core; +using BDArmory.Damage; +using BDArmory.Extensions; +using BDArmory.FX; using BDArmory.GameModes; -using BDArmory.Misc; using BDArmory.Modules; using BDArmory.Radar; +using BDArmory.Settings; using BDArmory.UI; -using UnityEngine; -using KSP.Localization; -using BDArmory.FX; +using BDArmory.Utils; +using BDArmory.VesselSpawning; +using BDArmory.Weapons.Missiles; +using BDArmory.Weapons; namespace BDArmory.Competition { - public class CompetitionScores - { - #region Public fields - public Dictionary ScoreData = new Dictionary(); - public Dictionary.KeyCollection Players => ScoreData.Keys; // Convenience variable - public int deathCount = 0; - public List deathOrder = new List(); // The names of dead players ordered by their death. - public string currentlyIT = ""; - #endregion - - #region Helper functions for registering hits, etc. - /// - /// Configure the scoring structure (wipes a previous one). - /// If a piñata is involved, include it here too. - /// - /// List of vessels involved in the competition. - public void ConfigurePlayers(List vessels) - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) { foreach (var vessel in vessels) { Debug.Log("[BDArmory.BDACompetitionMode.Scores]: Adding Score Tracker For " + vessel.vesselName); } } - ScoreData = vessels.ToDictionary(v => v.vesselName, v => new ScoringData()); - foreach (var vessel in vessels) - { - ScoreData[vessel.vesselName].team = VesselModuleRegistry.GetMissileFire(vessel, true).Team.Name; - } - deathCount = 0; - deathOrder.Clear(); - currentlyIT = ""; - } - /// - /// Add a competitor after the competition has started. - /// - /// - public bool AddPlayer(Vessel vessel) - { - if (ScoreData.ContainsKey(vessel.vesselName)) return false; // They're already there. - if (BDACompetitionMode.Instance.IsValidVessel(vessel) != BDACompetitionMode.InvalidVesselReason.None) return false; // Invalid vessel. - ScoreData[vessel.vesselName] = new ScoringData(); - ScoreData[vessel.vesselName].team = VesselModuleRegistry.GetMissileFire(vessel, true).Team.Name; - ScoreData[vessel.vesselName].lastFiredTime = Planetarium.GetUniversalTime(); - ScoreData[vessel.vesselName].previousPartCount = vessel.parts.Count(); - BDACompetitionMode.Instance.AddPlayerToRammingInformation(vessel); - return true; - } - /// - /// Remove a player from the competition. - /// - /// - /// - public bool RemovePlayer(string player) - { - if (!Players.Contains(player)) return false; - ScoreData.Remove(player); - BDACompetitionMode.Instance.RemovePlayerFromRammingInformation(player); - return true; - } - /// - /// Register a shot fired. - /// - /// The shooting vessel - /// true if successfully registered, false otherwise - public bool RegisterShot(string shooter) - { - if (!BDACompetitionMode.Instance.competitionIsActive) return false; - if (shooter == null || !ScoreData.ContainsKey(shooter)) return false; - if (ScoreData[shooter].aliveState != AliveState.Alive) return false; // Ignore shots fired after the vessel is dead. - ++ScoreData[shooter].shotsFired; - if (BDArmorySettings.RUNWAY_PROJECT) - { - if (BDArmorySettings.RUNWAY_PROJECT_ROUND == 41 && !BDACompetitionMode.Instance.s4r1FiringRateUpdatedFromShotThisFrame) - { - BDArmorySettings.FIRE_RATE_OVERRIDE += Mathf.Round(VectorUtils.Gaussian() * BDArmorySettings.FIRE_RATE_OVERRIDE_SPREAD + (BDArmorySettings.FIRE_RATE_OVERRIDE_CENTER - BDArmorySettings.FIRE_RATE_OVERRIDE) * BDArmorySettings.FIRE_RATE_OVERRIDE_BIAS * BDArmorySettings.FIRE_RATE_OVERRIDE_BIAS); - BDArmorySettings.FIRE_RATE_OVERRIDE = Mathf.Max(BDArmorySettings.FIRE_RATE_OVERRIDE, 10f); - BDACompetitionMode.Instance.s4r1FiringRateUpdatedFromShotThisFrame = true; - } - } - return true; - } - /// - /// Register a bullet hit. - /// - /// The attacking vessel - /// The victim vessel - /// The name of the weapon that fired the projectile - /// The distance travelled by the projectile - /// true if successfully registered, false otherwise - public bool RegisterBulletHit(string attacker, string victim, string weaponName = "", double distanceTraveled = 0) - { - if (!BDACompetitionMode.Instance.competitionIsActive) return false; - if (attacker == null || victim == null || attacker == victim || !ScoreData.ContainsKey(attacker) || !ScoreData.ContainsKey(victim)) return false; - if (ScoreData[victim].aliveState != AliveState.Alive) return false; // Ignore hits after the victim is dead. - - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log($"[BDArmory.BDACompetitionMode.Scores]: {attacker} scored a hit against {victim} with {weaponName} from a distance of {distanceTraveled}m."); - - var now = Planetarium.GetUniversalTime(); - - // Attacker stats. - ++ScoreData[attacker].hits; - if (victim == "pinata") ++ScoreData[attacker].PinataHits; - - // Victim stats. - if (ScoreData[victim].lastPersonWhoDamagedMe != attacker) - { - ScoreData[victim].previousLastDamageTime = ScoreData[victim].lastDamageTime; - ScoreData[victim].previousPersonWheDamagedMe = ScoreData[victim].lastPersonWhoDamagedMe; - } - if (ScoreData[victim].hitCounts.ContainsKey(attacker)) { ++ScoreData[victim].hitCounts[attacker]; } - else { ScoreData[victim].hitCounts[attacker] = 1; } - ScoreData[victim].lastDamageTime = now; - ScoreData[victim].lastDamageWasFrom = DamageFrom.Guns; - ScoreData[victim].lastPersonWhoDamagedMe = attacker; - ScoreData[victim].everyoneWhoDamagedMe.Add(attacker); - ScoreData[victim].damageTypesTaken.Add(DamageFrom.Guns); - - if (BDArmorySettings.REMOTE_LOGGING_ENABLED) - { BDAScoreService.Instance.TrackHit(attacker, victim, weaponName, distanceTraveled); } - - if (BDArmorySettings.TAG_MODE && !string.IsNullOrEmpty(weaponName)) // Empty weapon name indicates fire or other effect that doesn't count for tag mode. - { - if (ScoreData[victim].tagIsIt || string.IsNullOrEmpty(currentlyIT)) - { - if (ScoreData[victim].tagIsIt) - { - UpdateITTimeAndScore(); // Register time the victim spent as IT. - } - RegisterIsIT(attacker); // Register the attacker as now being IT. - } - } - - if (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 41 && !BDACompetitionMode.Instance.s4r1FiringRateUpdatedFromHitThisFrame) - { - BDArmorySettings.FIRE_RATE_OVERRIDE = Mathf.Round(Mathf.Min(BDArmorySettings.FIRE_RATE_OVERRIDE * BDArmorySettings.FIRE_RATE_OVERRIDE_HIT_MULTIPLIER, 1200f)); - BDACompetitionMode.Instance.s4r1FiringRateUpdatedFromHitThisFrame = true; - } - - return true; - } - /// - /// Register damage from bullets. - /// - /// Attacking vessel - /// Victim vessel - /// Amount of damage - /// true if successfully registered, false otherwise - public bool RegisterBulletDamage(string attacker, string victim, float damage) - { - if (!BDACompetitionMode.Instance.competitionIsActive) return false; - if (damage <= 0 || attacker == null || victim == null || attacker == victim || !ScoreData.ContainsKey(attacker) || !ScoreData.ContainsKey(victim)) return false; - if (ScoreData[victim].aliveState != AliveState.Alive) return false; // Ignore damage after the victim is dead. - if (float.IsNaN(damage)) - { - Debug.LogError($"DEBUG {attacker} did NaN damage to {victim}!"); - return false; - } - - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log($"[BDArmory.BDACompetitionMode.Scores]: {attacker} did {damage} damage to {victim} with a gun."); - - if (ScoreData[victim].damageFromGuns.ContainsKey(attacker)) { ScoreData[victim].damageFromGuns[attacker] += damage; } - else { ScoreData[victim].damageFromGuns[attacker] = damage; } - - if (BDArmorySettings.REMOTE_LOGGING_ENABLED) - { BDAScoreService.Instance.TrackDamage(attacker, victim, damage); } - return true; - } - /// - /// Register a rocket fired. - /// - /// The shooting vessel - /// true if successfully registered, false otherwise - public bool RegisterRocketFired(string shooter) - { - if (!BDACompetitionMode.Instance.competitionIsActive) return false; - if (shooter == null || !ScoreData.ContainsKey(shooter)) return false; - if (ScoreData[shooter].aliveState != AliveState.Alive) return false; // Ignore shots fired after the vessel is dead. - ++ScoreData[shooter].rocketsFired; - return true; - } - /// - /// Register individual rocket strikes. - /// Note: this includes both kinetic and explosive strikes, so a single rocket may count for two strikes. - /// - /// - /// - /// - public bool RegisterRocketStrike(string attacker, string victim) - { - if (!BDACompetitionMode.Instance.competitionIsActive) return false; - if (attacker == null || victim == null || attacker == victim || !ScoreData.ContainsKey(attacker) || !ScoreData.ContainsKey(victim)) return false; - if (ScoreData[victim].aliveState != AliveState.Alive) return false; // Ignore hits after the victim is dead. - - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log($"[BDArmory.BDACompetitionMode.Scores]: {attacker} scored a rocket strike against {victim}."); - - ++ScoreData[attacker].rocketStrikes; - if (ScoreData[victim].rocketStrikeCounts.ContainsKey(attacker)) { ++ScoreData[victim].rocketStrikeCounts[attacker]; } - else { ScoreData[victim].rocketStrikeCounts[attacker] = 1; } - - if (BDArmorySettings.REMOTE_LOGGING_ENABLED) - { BDAScoreService.Instance.TrackRocketStrike(attacker, victim); } - return true; - } - /// - /// Register the number of parts on the victim that were damaged by the attacker's rocket. - /// - /// - /// - /// - /// - public bool RegisterRocketHit(string attacker, string victim, int partsHit = 1) - { - if (!BDACompetitionMode.Instance.competitionIsActive) return false; - if (partsHit <= 0 || attacker == null || victim == null || attacker == victim || !ScoreData.ContainsKey(attacker) || !ScoreData.ContainsKey(victim)) return false; - if (ScoreData[victim].aliveState != AliveState.Alive) return false; // Ignore hits after the victim is dead. - - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log($"[BDArmory.BDACompetitionMode.Scores]: {attacker} damaged {partsHit} parts on {victim} with a rocket."); - - var now = Planetarium.GetUniversalTime(); - - // Attacker stats. - ScoreData[attacker].totalDamagedPartsDueToRockets += partsHit; - - // Victim stats. - if (ScoreData[victim].lastPersonWhoDamagedMe != attacker) - { - ScoreData[victim].previousLastDamageTime = ScoreData[victim].lastDamageTime; - ScoreData[victim].previousPersonWheDamagedMe = ScoreData[victim].lastPersonWhoDamagedMe; - } - if (ScoreData[victim].rocketPartDamageCounts.ContainsKey(attacker)) { ScoreData[victim].rocketPartDamageCounts[attacker] += partsHit; } - else { ScoreData[victim].rocketPartDamageCounts[attacker] = partsHit; } - ScoreData[victim].lastDamageTime = now; - ScoreData[victim].lastDamageWasFrom = DamageFrom.Rockets; - ScoreData[victim].lastPersonWhoDamagedMe = attacker; - ScoreData[victim].everyoneWhoDamagedMe.Add(attacker); - ScoreData[victim].damageTypesTaken.Add(DamageFrom.Rockets); - - if (BDArmorySettings.REMOTE_LOGGING_ENABLED) - { BDAScoreService.Instance.TrackRocketParts(attacker, victim, partsHit); } - return true; - } - /// - /// Register damage from rocket strikes. - /// - /// - /// - /// - /// - public bool RegisterRocketDamage(string attacker, string victim, float damage) - { - if (!BDACompetitionMode.Instance.competitionIsActive) return false; - if (damage <= 0 || attacker == null || victim == null || attacker == victim || !ScoreData.ContainsKey(attacker) || !ScoreData.ContainsKey(victim)) return false; - if (ScoreData[victim].aliveState != AliveState.Alive) return false; // Ignore damage after the victim is dead. - - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log($"[BDArmory.BDACompetitionMode.Scores]: {attacker} did {damage} damage to {victim} with a rocket."); - - if (ScoreData[victim].damageFromRockets.ContainsKey(attacker)) { ScoreData[victim].damageFromRockets[attacker] += damage; } - else { ScoreData[victim].damageFromRockets[attacker] = damage; } - - if (BDArmorySettings.REMOTE_LOGGING_ENABLED) - { BDAScoreService.Instance.TrackRocketDamage(attacker, victim, damage); } - return true; - } - /// - /// Register damage from Battle Damage. - /// - /// - /// - /// - /// - public bool RegisterBattleDamage(string attacker, Vessel victimVessel, float damage) - { - if (!BDACompetitionMode.Instance.competitionIsActive) return false; - if (victimVessel == null) return false; - var victim = victimVessel.vesselName; - if (damage <= 0 || attacker == null || victim == null || !ScoreData.ContainsKey(attacker) || !ScoreData.ContainsKey(victim)) return false; // Note: we allow attacker=victim here to track self damage. - if (ScoreData[victim].aliveState != AliveState.Alive) return false; // Ignore damage after the victim is dead. - if (VesselModuleRegistry.GetModuleCount(victimVessel) == 0) return false; // The victim is dead, but hasn't been registered as such yet. We want to check this here as it's common for BD to occur as the vessel is killed. - - if (ScoreData[victim].battleDamageFrom.ContainsKey(attacker)) { ScoreData[victim].battleDamageFrom[attacker] += damage; } - else { ScoreData[victim].battleDamageFrom[attacker] = damage; } - - return true; - } - /// - /// Register parts lost due to ram. - /// - /// - /// - /// time the ram occured, which may be before the most recently registered damage from other sources - /// - /// true if successfully registered, false otherwise - public bool RegisterRam(string attacker, string victim, double timeOfCollision, int partsLost) - { - if (!BDACompetitionMode.Instance.competitionIsActive) return false; - if (partsLost <= 0 || attacker == null || victim == null || attacker == victim || !ScoreData.ContainsKey(attacker) || !ScoreData.ContainsKey(victim)) return false; - if (ScoreData[victim].aliveState != AliveState.Alive) return false; // Ignore rams after the victim is dead. - - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log($"[BDArmory.BDACompetitionMode.Scores]: {attacker} rammed {victim} at {timeOfCollision} and the victim lost {partsLost} parts."); - - // Attacker stats. - ScoreData[attacker].totalDamagedPartsDueToRamming += partsLost; - - // Victim stats. - if (ScoreData[victim].lastDamageTime < timeOfCollision && ScoreData[victim].lastPersonWhoDamagedMe != attacker) - { - ScoreData[victim].previousLastDamageTime = ScoreData[victim].lastDamageTime; - ScoreData[victim].previousPersonWheDamagedMe = ScoreData[victim].lastPersonWhoDamagedMe; - } - else if (ScoreData[victim].previousLastDamageTime < timeOfCollision && ScoreData[victim].previousPersonWheDamagedMe != attacker) // Newer than the current previous last damage, but older than the most recent damage from someone else. - { - ScoreData[victim].previousLastDamageTime = timeOfCollision; - ScoreData[victim].previousPersonWheDamagedMe = attacker; - } - if (ScoreData[victim].rammingPartLossCounts.ContainsKey(attacker)) { ScoreData[victim].rammingPartLossCounts[attacker] += partsLost; } - else { ScoreData[victim].rammingPartLossCounts[attacker] = partsLost; } - if (ScoreData[victim].lastDamageTime < timeOfCollision) - { - ScoreData[victim].lastDamageTime = timeOfCollision; - ScoreData[victim].lastDamageWasFrom = DamageFrom.Ramming; - ScoreData[victim].lastPersonWhoDamagedMe = attacker; - } - ScoreData[victim].everyoneWhoDamagedMe.Add(attacker); - ScoreData[victim].damageTypesTaken.Add(DamageFrom.Ramming); - - if (BDArmorySettings.REMOTE_LOGGING_ENABLED) - { BDAScoreService.Instance.TrackRammedParts(attacker, victim, partsLost); } - return true; - } - /// - /// Register individual missile strikes. - /// - /// The vessel that launched the missile. - /// The struck vessel. - /// true if successfully registered, false otherwise - public bool RegisterMissileStrike(string attacker, string victim) - { - if (!BDACompetitionMode.Instance.competitionIsActive) return false; - if (attacker == null || victim == null || attacker == victim || !ScoreData.ContainsKey(attacker) || !ScoreData.ContainsKey(victim)) return false; - if (ScoreData[victim].aliveState != AliveState.Alive) return false; // Ignore hits after the victim is dead. - - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log($"[BDArmory.BDACompetitionMode.Scores]: {attacker} scored a missile strike against {victim}."); - - if (ScoreData[victim].missileHitCounts.ContainsKey(attacker)) { ++ScoreData[victim].missileHitCounts[attacker]; } - else { ScoreData[victim].missileHitCounts[attacker] = 1; } - - if (BDArmorySettings.REMOTE_LOGGING_ENABLED) - { BDAScoreService.Instance.TrackMissileStrike(attacker, victim); } - return true; - } - /// - /// Register the number of parts on the victim that were damaged by the attacker's missile. - /// - /// The vessel that launched the missile - /// The struck vessel - /// The number of parts hit (can be 1 at a time) - /// true if successfully registered, false otherwise - public bool RegisterMissileHit(string attacker, string victim, int partsHit = 1) - { - if (!BDACompetitionMode.Instance.competitionIsActive) return false; - if (partsHit <= 0 || attacker == null || victim == null || attacker == victim || !ScoreData.ContainsKey(attacker) || !ScoreData.ContainsKey(victim)) return false; - if (ScoreData[victim].aliveState != AliveState.Alive) return false; // Ignore hits after the victim is dead. - - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log($"[BDArmory.BDACompetitionMode.Scores]: {attacker} damaged {partsHit} parts on {victim} with a missile."); - - var now = Planetarium.GetUniversalTime(); - - // Attacker stats. - ScoreData[attacker].totalDamagedPartsDueToMissiles += partsHit; - - // Victim stats. - if (ScoreData[victim].lastPersonWhoDamagedMe != attacker) - { - ScoreData[victim].previousLastDamageTime = ScoreData[victim].lastDamageTime; - ScoreData[victim].previousPersonWheDamagedMe = ScoreData[victim].lastPersonWhoDamagedMe; - } - if (ScoreData[victim].missilePartDamageCounts.ContainsKey(attacker)) { ScoreData[victim].missilePartDamageCounts[attacker] += partsHit; } - else { ScoreData[victim].missilePartDamageCounts[attacker] = partsHit; } - ScoreData[victim].lastDamageTime = now; - ScoreData[victim].lastDamageWasFrom = DamageFrom.Missiles; - ScoreData[victim].lastPersonWhoDamagedMe = attacker; - ScoreData[victim].everyoneWhoDamagedMe.Add(attacker); - ScoreData[victim].damageTypesTaken.Add(DamageFrom.Missiles); - - if (BDArmorySettings.REMOTE_LOGGING_ENABLED) - { BDAScoreService.Instance.TrackMissileParts(attacker, victim, partsHit); } - return true; - } - /// - /// Register damage from missile strikes. - /// - /// The vessel that launched the missile - /// The struck vessel - /// The amount of damage done - /// true if successfully registered, false otherwise - public bool RegisterMissileDamage(string attacker, string victim, float damage) - { - if (!BDACompetitionMode.Instance.competitionIsActive) return false; - if (damage <= 0 || attacker == null || victim == null || attacker == victim || !ScoreData.ContainsKey(attacker) || !ScoreData.ContainsKey(victim)) return false; - if (ScoreData[victim].aliveState != AliveState.Alive) return false; // Ignore damage after the victim is dead. - - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log($"[BDArmory.BDACompetitionMode.Scores]: {attacker} did {damage} damage to {victim} with a missile."); - - if (ScoreData[victim].damageFromMissiles.ContainsKey(attacker)) { ScoreData[victim].damageFromMissiles[attacker] += damage; } - else { ScoreData[victim].damageFromMissiles[attacker] = damage; } - - if (BDArmorySettings.REMOTE_LOGGING_ENABLED) - { BDAScoreService.Instance.TrackMissileDamage(attacker, victim, damage); } - return true; - } - /// - /// Register a vessel dying. - /// - /// - /// true if successfully registered, false otherwise - public bool RegisterDeath(string vesselName, GMKillReason gmKillReason = GMKillReason.None, double timeOfDeath = -1) - { - if (!BDACompetitionMode.Instance.competitionIsActive) return false; - if (vesselName == null || !ScoreData.ContainsKey(vesselName)) return false; - if (ScoreData[vesselName].aliveState != AliveState.Alive) return false; // They're already dead! - - var now = timeOfDeath < 0 ? Planetarium.GetUniversalTime() : timeOfDeath; - var deathTimes = ScoreData.Values.Select(s => s.deathTime).ToList(); - var fixDeathOrder = timeOfDeath > -1 && deathTimes.Count > 0 && timeOfDeath - BDACompetitionMode.Instance.competitionStartTime < deathTimes.Max(); - deathOrder.Add(vesselName); - ScoreData[vesselName].deathOrder = deathCount++; - ScoreData[vesselName].deathTime = now - BDACompetitionMode.Instance.competitionStartTime; - ScoreData[vesselName].gmKillReason = gmKillReason; - if (fixDeathOrder) // Fix the death order if needed. - { - deathOrder = ScoreData.Where(s => s.Value.deathTime > -1).OrderBy(s => s.Value.deathTime).Select(s => s.Key).ToList(); - for (int i = 0; i < deathOrder.Count; ++i) - ScoreData[deathOrder[i]].deathOrder = i; - } - - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log($"[BDArmory.BDACompetitionMode.Scores]: {vesselName} died at {ScoreData[vesselName].deathTime} (position {ScoreData[vesselName].deathOrder}), GM reason: {gmKillReason}, last damage from: {ScoreData[vesselName].lastDamageWasFrom}"); - - if (BDArmorySettings.REMOTE_LOGGING_ENABLED) - { BDAScoreService.Instance.TrackDeath(vesselName); } - - if (BDArmorySettings.TAG_MODE) - { - if (ScoreData[vesselName].tagIsIt) - { - UpdateITTimeAndScore(); // Update the final IT time for the vessel. - ScoreData[vesselName].tagIsIt = false; // Register the vessel as no longer IT. - if (gmKillReason == GMKillReason.None) // If it wasn't a GM kill, set the previous vessel that hit this one as IT. - { RegisterIsIT(ScoreData[vesselName].lastPersonWhoDamagedMe); } - else - { currentlyIT = ""; } - if (string.IsNullOrEmpty(currentlyIT)) // GM kill or couldn't find a someone else to be IT. - { BDACompetitionMode.Instance.TagResetTeams(); } - } - else if (ScoreData.ContainsKey(ScoreData[vesselName].lastPersonWhoDamagedMe) && ScoreData[ScoreData[vesselName].lastPersonWhoDamagedMe].tagIsIt) // Check to see if the IT vessel killed them. - { ScoreData[ScoreData[vesselName].lastPersonWhoDamagedMe].tagKillsWhileIt++; } - } - - if (ScoreData[vesselName].lastDamageWasFrom == DamageFrom.None) // Died without being hit by anyone => Incompetence - { - ScoreData[vesselName].aliveState = AliveState.Dead; - if (gmKillReason == GMKillReason.None) - { ScoreData[vesselName].lastDamageWasFrom = DamageFrom.Incompetence; } - return true; - } - - if (now - ScoreData[vesselName].lastDamageTime < BDArmorySettings.SCORING_HEADSHOT && ScoreData[vesselName].gmKillReason == GMKillReason.None) // Died shortly after being hit (and not by the GM) - { - if (ScoreData[vesselName].previousLastDamageTime < 0) // No-one else hit them => Clean kill - { ScoreData[vesselName].aliveState = AliveState.CleanKill; } - else if (now - ScoreData[vesselName].previousLastDamageTime > BDArmorySettings.SCORING_KILLSTEAL) // Last hit from someone else was a while ago => Head-shot - { ScoreData[vesselName].aliveState = AliveState.HeadShot; } - else // Last hit from someone else was recent => Kill Steal - { ScoreData[vesselName].aliveState = AliveState.KillSteal; } - - if (BDArmorySettings.REMOTE_LOGGING_ENABLED) - { BDAScoreService.Instance.TrackKill(ScoreData[vesselName].lastPersonWhoDamagedMe, vesselName); } - } - else // Survived for a while after being hit or GM kill => Assist - { - ScoreData[vesselName].aliveState = AliveState.AssistedKill; - - if (BDArmorySettings.REMOTE_LOGGING_ENABLED) - { BDAScoreService.Instance.ComputeAssists(vesselName, "", now - BDACompetitionMode.Instance.competitionStartTime); } - } - - if (BDArmorySettings.VESSEL_SPAWN_DUMP_LOG_EVERY_SPAWN && ContinuousSpawning.Instance.vesselsSpawningContinuously) ContinuousSpawning.Instance.DumpContinuousSpawningScores(); - - return true; - } - - #region Tag - public bool RegisterIsIT(string vesselName) - { - if (string.IsNullOrEmpty(vesselName) || !ScoreData.ContainsKey(vesselName)) - { - currentlyIT = ""; - return false; - } - - var now = Planetarium.GetUniversalTime(); - var vessels = BDACompetitionMode.Instance.GetAllPilots().Select(pilot => pilot.vessel).Where(vessel => Players.Contains(vessel.vesselName)).ToDictionary(vessel => vessel.vesselName, vessel => vessel); // Get the vessels so we can trigger action groups on them. Also checks that the vessels are valid competitors. - if (vessels.ContainsKey(vesselName)) // Set the player as IT if they're alive. - { - currentlyIT = vesselName; - ScoreData[vesselName].tagIsIt = true; - ScoreData[vesselName].tagTimesIt++; - ScoreData[vesselName].tagLastUpdated = now; - var mf = VesselModuleRegistry.GetMissileFire(vessels[vesselName]); - mf.SetTeam(BDTeam.Get("IT")); - mf.ForceScan(); - BDACompetitionMode.Instance.competitionStatus.Add(vesselName + " is IT!"); - vessels[vesselName].ActionGroups.ToggleGroup(BDACompetitionMode.KM_dictAG[8]); // Trigger AG8 on becoming "IT" - } - else { currentlyIT = ""; } - foreach (var player in Players) // Make sure other players are not NOT IT. - { - if (player != vesselName && vessels.ContainsKey(player)) - { - if (ScoreData[player].team != "NO") - { - ScoreData[player].tagIsIt = false; - var mf = VesselModuleRegistry.GetMissileFire(vessels[player]); - mf.SetTeam(BDTeam.Get("NO")); - mf.ForceScan(); - vessels[player].ActionGroups.ToggleGroup(BDACompetitionMode.KM_dictAG[9]); // Trigger AG9 on becoming "NOT IT" - } - } - } - return true; - } - public bool UpdateITTimeAndScore() - { - if (!string.IsNullOrEmpty(currentlyIT)) - { - if (BDACompetitionMode.Instance.previousNumberCompetitive < 2 || ScoreData[currentlyIT].landedState) return false; // Don't update if there are no competitors or we're landed. - var now = Planetarium.GetUniversalTime(); - ScoreData[currentlyIT].tagTotalTime += now - ScoreData[currentlyIT].tagLastUpdated; - ScoreData[currentlyIT].tagScore += (now - ScoreData[currentlyIT].tagLastUpdated) * BDACompetitionMode.Instance.previousNumberCompetitive * (BDACompetitionMode.Instance.previousNumberCompetitive - 1) / 5; // Rewards craft accruing time with more competitors - ScoreData[currentlyIT].tagLastUpdated = now; - } - return true; - } - #endregion - - #region Waypoints - public bool RegisterWaypointReached(string vesselName, int waypointIndex, float distance) - { - if (!BDACompetitionMode.Instance.competitionIsActive) return false; - if (vesselName == null || !ScoreData.ContainsKey(vesselName)) return false; - - ScoreData[vesselName].waypointsReached.Add(new ScoringData.WaypointReached(waypointIndex, distance, Planetarium.GetUniversalTime() - BDACompetitionMode.Instance.competitionStartTime)); - var displayName = vesselName; - if (BDArmorySettings.ENABLE_HOS && BDArmorySettings.HALL_OF_SHAME_LIST.Contains(vesselName) && !string.IsNullOrEmpty(BDArmorySettings.HOS_BADGE)) - { - displayName += " (" + BDArmorySettings.HOS_BADGE + ")"; - } - BDACompetitionMode.Instance.competitionStatus.Add($"{displayName}: Waypoint {waypointIndex} reached! Time: {ScoreData[vesselName].waypointsReached.Last().timestamp - ScoreData[vesselName].waypointsReached.First().timestamp:F2}s, Deviation: {distance:F1}m"); - - return true; - } - #endregion - #endregion - - public void LogResults(string CompetitionID, string message = "", string tag = "") - { - var logStrings = new List(); - logStrings.Add("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: Dumping Results" + (message != "" ? " " + message : "") + " after " + (int)(Planetarium.GetUniversalTime() - BDACompetitionMode.Instance.competitionStartTime) + "s (of " + (BDArmorySettings.COMPETITION_DURATION * 60d) + "s) at " + DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss zzz")); - - // Find out who's still alive - var alive = new HashSet(); - var survivingTeams = new HashSet(); - foreach (var vessel in FlightGlobals.Vessels) - { - if (vessel == null || !vessel.loaded || vessel.packed || VesselModuleRegistry.ignoredVesselTypes.Contains(vessel.vesselType)) - continue; - var mf = VesselModuleRegistry.GetModule(vessel); - double HP = 0; - double WreckFactor = 0; - if (mf != null) - { - HP = (mf.currentHP / mf.totalHP) * 100; - if (ScoreData.ContainsKey(vessel.vesselName)) - { - ScoreData[vessel.vesselName].remainingHP = HP; - survivingTeams.Add(ScoreData[vessel.vesselName].team); //move this here so last man standing can claim the win, even if they later don't meet the 'survive' criteria - } - if (HP < 100) - { - WreckFactor += (100 - HP) / 100; //the less plane remaining, the greater the chance it's a wreck - } - if (vessel.verticalSpeed < -30) //falling out of the sky? Could be an intact plane diving to default alt, could be a cockpit - { - WreckFactor += 0.5f; - var AI = VesselModuleRegistry.GetBDModulePilotAI(vessel, true); - if (AI == null || vessel.radarAltitude < AI.defaultAltitude) //craft is uncontrollably diving, not returning from high alt to cruising alt - { - WreckFactor += 0.5f; - } - } - if (VesselModuleRegistry.GetModuleCount(vessel) > 0) - { - int engineOut = 0; - foreach (var engine in VesselModuleRegistry.GetModules(vessel)) - { - if (engine == null || engine.flameout || engine.finalThrust <= 0) - engineOut++; - } - WreckFactor += (engineOut / VesselModuleRegistry.GetModuleCount(vessel)) / 2; - } - else - { - WreckFactor += 0.5f; //could be a glider, could be missing engines - } - if (WreckFactor < 1.1f) // 'wrecked' requires some combination of diving, no engines, and missing parts - { - alive.Add(vessel.vesselName); - } - } - } - - // General result. (Note: uses hand-coded JSON to make parsing easier in python.) - if (survivingTeams.Count == 0) - { - logStrings.Add("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: RESULT:Mutual Annihilation"); - } - else if (survivingTeams.Count == 1) - { // Win - var winningTeam = survivingTeams.First(); - var winningTeamMembers = ScoreData.Where(s => s.Value.team == winningTeam).Select(s => s.Key); - logStrings.Add("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: RESULT:Win:{\"team\": " + $"\"{winningTeam}\", \"members\": [" + string.Join(", ", winningTeamMembers.Select(m => $"\"{m.Replace("\"", "\\\"")}\"")) + "]}"); - } - else - { // Draw - var drawTeams = survivingTeams.ToDictionary(t => t, t => ScoreData.Where(s => s.Value.team == t).Select(s => s.Key)); - logStrings.Add("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: RESULT:Draw:[" + string.Join(", ", drawTeams.Select(t => "{\"team\": " + $"\"{t.Key}\"" + ", \"members\": [" + string.Join(", ", t.Value.Select(m => $"\"{m.Replace("\"", "\\\"")}\"")) + "]}")) + "]"); - } - { // Dead teams. - var deadTeamNames = ScoreData.Where(s => !survivingTeams.Contains(s.Value.team)).Select(s => s.Value.team).ToHashSet(); - var deadTeams = deadTeamNames.ToDictionary(t => t, t => ScoreData.Where(s => s.Value.team == t).Select(s => s.Key)); - logStrings.Add("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: DEADTEAMS:[" + string.Join(", ", deadTeams.Select(t => "{\"team\": " + $"\"{t.Key}\"" + ", \"members\": [" + string.Join(", ", t.Value.Select(m => $"\"{m.Replace("\"", "\\\"")}\"")) + "]}")) + "]"); - } - - // Record ALIVE/DEAD status of each craft. - foreach (var vesselName in alive) // List ALIVE craft first - { - logStrings.Add("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: ALIVE:" + vesselName); - } - foreach (var player in Players) // Then DEAD or MIA. - { - if (!alive.Contains(player)) - { - if (ScoreData[player].deathOrder > -1) - { - logStrings.Add("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: DEAD:" + ScoreData[player].deathOrder + ":" + ScoreData[player].deathTime.ToString("0.0") + ":" + player); // DEAD: :: - } - else - { - logStrings.Add("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: MIA:" + player); - } - } - } - - // Report survivors to Remote Orchestration - if (BDArmorySettings.REMOTE_LOGGING_ENABLED) - { BDAScoreService.Instance.TrackSurvivors(Players.Where(player => ScoreData[player].deathOrder == -1).ToList()); } - - // Who shot who. - foreach (var player in Players) - if (ScoreData[player].hitCounts.Count > 0) - { - string whoShotMe = "[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: WHOSHOTWHOWITHGUNS:" + player; - foreach (var vesselName in ScoreData[player].hitCounts.Keys) - whoShotMe += ":" + ScoreData[player].hitCounts[vesselName] + ":" + vesselName; - logStrings.Add(whoShotMe); - } - - // Damage from bullets - foreach (var player in Players) - if (ScoreData[player].damageFromGuns.Count > 0) - { - string whoDamagedMeWithGuns = "[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: WHODAMAGEDWHOWITHGUNS:" + player; - foreach (var vesselName in ScoreData[player].damageFromGuns.Keys) - whoDamagedMeWithGuns += ":" + ScoreData[player].damageFromGuns[vesselName].ToString("0.0") + ":" + vesselName; - logStrings.Add(whoDamagedMeWithGuns); - } - - // Who hit who with rockets. - foreach (var player in Players) - if (ScoreData[player].rocketStrikeCounts.Count > 0) - { - string whoHitMeWithRockets = "[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: WHOHITWHOWITHROCKETS:" + player; - foreach (var vesselName in ScoreData[player].rocketStrikeCounts.Keys) - whoHitMeWithRockets += ":" + ScoreData[player].rocketStrikeCounts[vesselName] + ":" + vesselName; - logStrings.Add(whoHitMeWithRockets); - } - - // Who hit parts by who with rockets. - foreach (var player in Players) - if (ScoreData[player].rocketPartDamageCounts.Count > 0) - { - string partHitCountsFromRockets = "[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: WHOPARTSHITWHOWITHROCKETS:" + player; - foreach (var vesselName in ScoreData[player].rocketPartDamageCounts.Keys) - partHitCountsFromRockets += ":" + ScoreData[player].rocketPartDamageCounts[vesselName] + ":" + vesselName; - logStrings.Add(partHitCountsFromRockets); - } - - // Damage from rockets - foreach (var player in Players) - if (ScoreData[player].damageFromRockets.Count > 0) - { - string whoDamagedMeWithRockets = "[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: WHODAMAGEDWHOWITHROCKETS:" + player; - foreach (var vesselName in ScoreData[player].damageFromRockets.Keys) - whoDamagedMeWithRockets += ":" + ScoreData[player].damageFromRockets[vesselName].ToString("0.0") + ":" + vesselName; - logStrings.Add(whoDamagedMeWithRockets); - } - - // Who hit who with missiles. - foreach (var player in Players) - if (ScoreData[player].missileHitCounts.Count > 0) - { - string whoHitMeWithMissiles = "[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: WHOHITWHOWITHMISSILES:" + player; - foreach (var vesselName in ScoreData[player].missileHitCounts.Keys) - whoHitMeWithMissiles += ":" + ScoreData[player].missileHitCounts[vesselName] + ":" + vesselName; - logStrings.Add(whoHitMeWithMissiles); - } - - // Who hit parts by who with missiles. - foreach (var player in Players) - if (ScoreData[player].missilePartDamageCounts.Count > 0) - { - string partHitCountsFromMissiles = "[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: WHOPARTSHITWHOWITHMISSILES:" + player; - foreach (var vesselName in ScoreData[player].missilePartDamageCounts.Keys) - partHitCountsFromMissiles += ":" + ScoreData[player].missilePartDamageCounts[vesselName] + ":" + vesselName; - logStrings.Add(partHitCountsFromMissiles); - } - - // Damage from missiles - foreach (var player in Players) - if (ScoreData[player].damageFromMissiles.Count > 0) - { - string whoDamagedMeWithMissiles = "[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: WHODAMAGEDWHOWITHMISSILES:" + player; - foreach (var vesselName in ScoreData[player].damageFromMissiles.Keys) - whoDamagedMeWithMissiles += ":" + ScoreData[player].damageFromMissiles[vesselName].ToString("0.0") + ":" + vesselName; - logStrings.Add(whoDamagedMeWithMissiles); - } - - // Who rammed who. - foreach (var player in Players) - if (ScoreData[player].rammingPartLossCounts.Count > 0) - { - string whoRammedMe = "[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: WHORAMMEDWHO:" + player; - foreach (var vesselName in ScoreData[player].rammingPartLossCounts.Keys) - whoRammedMe += ":" + ScoreData[player].rammingPartLossCounts[vesselName] + ":" + vesselName; - logStrings.Add(whoRammedMe); - } - - // Battle Damage - foreach (var player in Players) - if (ScoreData[player].battleDamageFrom.Count > 0) - { - string whoDamagedMeWithBattleDamages = "[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: WHODAMAGEDWHOWITHBATTLEDAMAGE:" + player; - foreach (var vesselName in ScoreData[player].battleDamageFrom.Keys) - whoDamagedMeWithBattleDamages += ":" + ScoreData[player].battleDamageFrom[vesselName].ToString("0.0") + ":" + vesselName; - logStrings.Add(whoDamagedMeWithBattleDamages); - } - - // GM kill reasons - foreach (var player in Players) - if (ScoreData[player].gmKillReason != GMKillReason.None) - logStrings.Add("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: GMKILL:" + player + ":" + ScoreData[player].gmKillReason); - - // Clean kills/rams/etc. - var specialKills = new HashSet { AliveState.CleanKill, AliveState.HeadShot, AliveState.KillSteal }; - foreach (var player in Players) - { - if (specialKills.Contains(ScoreData[player].aliveState) && ScoreData[player].gmKillReason == GMKillReason.None) - { - logStrings.Add("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: " + ScoreData[player].aliveState.ToString().ToUpper() + ScoreData[player].lastDamageWasFrom.ToString().ToUpper() + ":" + player + ":" + ScoreData[player].lastPersonWhoDamagedMe); - } - } - - // remaining health - foreach (var key in Players) - { - logStrings.Add("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: HPLEFT:" + key + ":" + ScoreData[key].remainingHP); - } - - // Accuracy - foreach (var player in Players) - { - logStrings.Add("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: ACCURACY:" + player + ":" + ScoreData[player].hits + "/" + ScoreData[player].shotsFired + ":" + ScoreData[player].rocketStrikes + "/" + ScoreData[player].rocketsFired); - } - - // Time "IT" and kills while "IT" logging - if (BDArmorySettings.TAG_MODE) - { - foreach (var player in Players) - logStrings.Add("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: TAGSCORE:" + player + ":" + ScoreData[player].tagScore.ToString("0.0")); - - foreach (var player in Players) - logStrings.Add("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: TIMEIT:" + player + ":" + ScoreData[player].tagTotalTime.ToString("0.0")); - - foreach (var player in Players) - if (ScoreData[player].tagKillsWhileIt > 0) - logStrings.Add("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: KILLSWHILEIT:" + player + ":" + ScoreData[player].tagKillsWhileIt); - - foreach (var player in Players) - if (ScoreData[player].tagTimesIt > 0) - logStrings.Add("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: TIMESIT:" + player + ":" + ScoreData[player].tagTimesIt); - } - - // Waypoints - foreach (var player in Players) - { - if (ScoreData[player].waypointsReached.Count > 0) - logStrings.Add("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: WAYPOINTS:" + player + ":" + string.Join(";", ScoreData[player].waypointsReached.Select(wp => wp.waypointIndex + ":" + wp.deviation.ToString("F2") + ":" + wp.timestamp.ToString("F2")))); - } - - // Dump the log results to a file - var folder = Path.GetFullPath(Path.Combine(KSPUtil.ApplicationRootPath, "GameData", "BDArmory", "Logs")); - if (BDATournament.Instance.tournamentStatus == TournamentStatus.Running) - { - folder = Path.Combine(folder, "Tournament " + BDATournament.Instance.tournamentID, "Round " + BDATournament.Instance.currentRound); - tag = "Heat " + BDATournament.Instance.currentHeat; - } - if (!Directory.Exists(folder)) - Directory.CreateDirectory(folder); - var fileName = Path.Combine(folder, CompetitionID.ToString() + (tag != "" ? "-" + tag : "") + ".log"); - Debug.Log($"[BDArmory.BDACompetitionMode]: Dumping competition results to {fileName}"); - File.WriteAllLines(fileName, logStrings); - } - } - - public class ScoringData - { - public AliveState aliveState = AliveState.Alive; // State of the vessel. - public string team; // The vessel's team. - - #region Guns - public int hits; // Number of hits this vessel landed. - public int PinataHits; // Number of hits this vessel landed on the piñata (included in Hits). - public int shotsFired = 0; // Number of shots fired by this vessel. - public Dictionary hitCounts = new Dictionary(); // Hits taken from guns fired by other vessels. - public Dictionary damageFromGuns = new Dictionary(); // Damage taken from guns fired by other vessels. - #endregion - - #region Rockets - public int totalDamagedPartsDueToRockets = 0; // Number of other vessels' parts damaged by this vessel due to rocket strikes. - public int rocketStrikes = 0; // Number of rockets fired by the vessel that hit someone. - public int rocketsFired = 0; // Number of rockets fired by this vessel. - public Dictionary damageFromRockets = new Dictionary(); // Damage taken from rocket hits from other vessels. - public Dictionary rocketPartDamageCounts = new Dictionary(); // Number of parts damaged by rocket hits from other vessels. - public Dictionary rocketStrikeCounts = new Dictionary(); // Number of rocket strikes from other vessels. - #endregion - - #region Ramming - public int totalDamagedPartsDueToRamming = 0; // Number of other vessels' parts destroyed by this vessel due to ramming. - public Dictionary rammingPartLossCounts = new Dictionary(); // Number of parts lost due to ramming by other vessels. - #endregion - - #region Missiles - public int totalDamagedPartsDueToMissiles = 0; // Number of other vessels' parts damaged by this vessel due to missile strikes. - public Dictionary damageFromMissiles = new Dictionary(); // Damage taken from missile strikes from other vessels. - public Dictionary missilePartDamageCounts = new Dictionary(); // Number of parts damaged by missile strikes from other vessels. - public Dictionary missileHitCounts = new Dictionary(); // Number of missile strikes from other vessels. - #endregion - - #region Battle Damage - public Dictionary battleDamageFrom = new Dictionary(); // Battle damage taken from others. - #endregion - - #region GM - public double lastFiredTime; // Time that this vessel last fired a gun. - public bool landedState; // Whether the vessel is landed or not. - public double lastLandedTime; // Time that this vessel was landed last. - public double landedKillTimer; // Counter tracking time this vessel is landed (for the kill timer). - public double AverageSpeed; // Average speed of this vessel recently (for the killer GM). - public double AverageAltitude; // Average altitude of this vessel recently (for the killer GM). - public int averageCount; // Count for the averaging stats. - public GMKillReason gmKillReason = GMKillReason.None; // Reason the GM killed this vessel. - #endregion - - #region Tag - public bool tagIsIt = false; // Whether this vessel is IT or not. - public int tagKillsWhileIt = 0; // The number of kills gained while being IT. - public int tagTimesIt = 0; // The number of times this vessel was IT. - public double tagTotalTime = 0; // The total this vessel spent being IT. - public double tagScore = 0; // Abstract score for tag mode. - public double tagLastUpdated = 0; // Time the tag time was last updated. - #endregion - - #region Waypoint - public struct WaypointReached - { - public WaypointReached(int waypointIndex, float deviation, double timestamp) { this.waypointIndex = waypointIndex; this.deviation = deviation; this.timestamp = timestamp; } - public int waypointIndex; // Number of waypoints this vessel reached. - public float deviation; // Deviation from waypoint. - public double timestamp; // Timestamp of reaching waypoint. - } - public List waypointsReached = new List(); - #endregion - - #region Misc - public int previousPartCount; // Number of parts this vessel had last time we checked (for tracking when a vessel has lost parts). - public double lastLostPartTime = 0; // Time of losing last part (up to granularity of the updateTickLength). - public double remainingHP; // HP of vessel - public double lastDamageTime = -1; - public DamageFrom lastDamageWasFrom = DamageFrom.None; - public string lastPersonWhoDamagedMe = ""; - public double previousLastDamageTime = -1; - public string previousPersonWheDamagedMe = ""; - public int deathOrder = -1; - public double deathTime = -1; - public HashSet damageTypesTaken = new HashSet(); - public HashSet everyoneWhoDamagedMe = new HashSet(); // Every other vessel that damaged this vessel. - #endregion - } - public enum DamageFrom { None, Guns, Rockets, Missiles, Ramming, Incompetence }; - public enum AliveState { Alive, CleanKill, HeadShot, KillSteal, AssistedKill, Dead }; - public enum GMKillReason { None, GM, OutOfAmmo, BigRedButton, LandedTooLong }; public enum CompetitionStartFailureReason { None, OnlyOneTeam, TeamsChanged, TeamLeaderDisappeared, PilotDisappeared, Other }; public enum CompetitionType { FFA, SEQUENCED, WAYPOINTS }; @@ -957,19 +32,24 @@ public class BDACompetitionMode : MonoBehaviour #region Flags and variables // Score tracking flags and variables. - public CompetitionScores Scores = new CompetitionScores(); // TODO Switch to using this for tracking scores instead. + public CompetitionScores Scores = new CompetitionScores(); // Competition flags and variables public CompetitionType competitionType = CompetitionType.FFA; public int CompetitionID; // time competition was started + public string competitionTag = ""; public double competitionStartTime = -1; public double MutatorResetTime = -1; public double competitionPreStartTime = -1; public double nextUpdateTick = -1; private double decisionTick = -1; private double finalGracePeriodStart = -1; + int competitiveTeamsAliveLimit = 2; + double altitudeLimitGracePeriod = -1; public static float gravityMultiplier = 1f; float lastGravityMultiplier; + public float MinAlt = 1f; + float lastMinAlt; private string deadOrAlive = ""; static HashSet outOfAmmo = new HashSet(); // outOfAmmo register for tracking which planes are out of ammo. @@ -1020,6 +100,8 @@ public class BDACompetitionMode : MonoBehaviour GUIStyle dateStyleShadow; Rect dateRect; Rect dateRectShadow; + Rect versionRect; + Rect versionRectShadow; string guiStatusString; #endregion @@ -1055,6 +137,8 @@ void OnGUI() string pDate = DateTime.UtcNow.ToString("yyyy-MM-dd\nHH:mm:ss") + " UTC"; GUI.Label(dateRectShadow, pDate, dateStyleShadow); GUI.Label(dateRect, pDate, dateStyle); + GUI.Label(versionRectShadow, BDArmorySetup.Version, dateStyleShadow); + GUI.Label(versionRect, BDArmorySetup.Version, dateStyle); } // Messages @@ -1124,10 +208,12 @@ public void UpdateGUIElements() statusStyle.fontStyle = FontStyle.Bold; statusStyle.alignment = TextAnchor.UpperLeft; dateStyle = new GUIStyle(statusStyle); + int shadowOffset = 2; if (BDArmorySetup.GAME_UI_ENABLED) { clockRect = new Rect(10, 42, 100, 30); dateRect = new Rect(100, 38, 100, 20); + versionRect = new Rect(200, 46, 100, 20); statusRect = new Rect(30, 80, Screen.width - 130, Mathf.FloorToInt(Screen.height / 2)); statusStyle.fontSize = 22; dateStyle.fontSize = 14; @@ -1136,19 +222,24 @@ public void UpdateGUIElements() { clockRect = new Rect(10, 6, 80, 20); dateRect = new Rect(10, 26, 100, 20); + versionRect = new Rect(10, 48, 100, 20); statusRect = new Rect(80, 6, Screen.width - 80, Mathf.FloorToInt(Screen.height / 2)); + shadowOffset = 1; statusStyle.fontSize = 14; dateStyle.fontSize = 10; } clockRectShadow = new Rect(clockRect); - clockRectShadow.x += 2; - clockRectShadow.y += 2; + clockRectShadow.x += shadowOffset; + clockRectShadow.y += shadowOffset; dateRectShadow = new Rect(dateRect); - dateRectShadow.x += 2; - dateRectShadow.y += 2; + dateRectShadow.x += shadowOffset; + dateRectShadow.y += shadowOffset; + versionRectShadow = new Rect(versionRect); + versionRectShadow.x += shadowOffset; + versionRectShadow.y += shadowOffset; statusRectShadow = new Rect(statusRect); - statusRectShadow.x += 2; - statusRectShadow.y += 2; + statusRectShadow.x += shadowOffset; + statusRectShadow.y += shadowOffset; statusStyleShadow = new GUIStyle(statusStyle); statusStyleShadow.normal.textColor = new Color(0, 0, 0, 0.75f); dateStyleShadow = new GUIStyle(dateStyle); @@ -1202,11 +293,11 @@ public void StartCompetitionNow(float delay = 0) } } - public void StartCompetitionMode(float distance) + public void StartCompetitionMode(float distance, bool startDespiteFailures = false, string tag = "") { if (!competitionStarting) { - ResetCompetitionStuff(); + ResetCompetitionStuff(tag); Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: Starting Competition"); startCompetitionNow = false; if (BDArmorySettings.GRAVITY_HACKS) @@ -1217,13 +308,14 @@ public void StartCompetitionMode(float distance) VehiclePhysics.Gravity.Refresh(); } RemoveDebrisNow(); + SpawnUtils.RestoreKALGlobally(BDArmorySettings.RESTORE_KAL); GameEvents.onVesselPartCountChanged.Add(OnVesselModified); GameEvents.onVesselCreate.Add(OnVesselModified); GameEvents.onCrewOnEva.Add(OnCrewOnEVA); if (BDArmorySettings.AUTO_ENABLE_VESSEL_SWITCHING) - LoadedVesselSwitcher.Instance.EnableAutoVesselSwitching(true); + LoadedVesselSwitcher.Instance.EnableAutoVesselSwitching(!hasPinata); competitionStartFailureReason = CompetitionStartFailureReason.None; - competitionRoutine = StartCoroutine(DogfightCompetitionModeRoutine(distance)); + competitionRoutine = StartCoroutine(DogfightCompetitionModeRoutine(distance, startDespiteFailures)); if (BDArmorySettings.COMPETITION_START_NOW_AFTER < 11) { if (BDArmorySettings.COMPETITION_START_NOW_AFTER > 5) @@ -1237,12 +329,14 @@ public void StartCompetitionMode(float distance) LoadedVesselSwitcher.Instance.StartVesselTracing(); if (BDArmorySettings.TIME_OVERRIDE && BDArmorySettings.TIME_SCALE != 0) { Time.timeScale = BDArmorySettings.TIME_SCALE; } + if (BDArmorySettings.VESSEL_MOVER_CLOSE_ON_COMPETITION_START && BDArmorySetup.showVesselMoverGUI) VesselMover.Instance.SetVisible(false); } } public void StopCompetition() { - LogResults(); + if (LoadedVesselSwitcher.Instance is not null) LoadedVesselSwitcher.Instance.ResetDeadVessels(); // Reset the dead vessels in the LVS so that the final corrected results are shown. + LogResults(tag: competitionTag); if (competitionIsActive && ContinuousSpawning.Instance.vesselsSpawningContinuously) { SpawnUtils.CancelSpawning(); @@ -1261,6 +355,7 @@ public void StopCompetition() sequencedCompetitionStarting = false; competitionStartTime = -1; competitionType = CompetitionType.FFA; + competitionTag = ""; if (PhysicsGlobals.GraviticForceMultiplier != 1) { lastGravityMultiplier = 1f; @@ -1273,7 +368,7 @@ public void StopCompetition() GameEvents.onVesselCreate.Remove(OnVesselModified); GameEvents.onCrewOnEva.Remove(OnCrewOnEVA); GameEvents.onVesselCreate.Remove(DebrisDelayedCleanUp); - GameEvents.onCometSpawned.Remove(RemoveCometVessel); + CometCleanup(); rammingInformation = null; // Reset the ramming information. deadOrAlive = ""; if (BDArmorySettings.TRACE_VESSELS_DURING_COMPETITIONS) @@ -1289,8 +384,7 @@ void CompetitionStarted() sequencedCompetitionStarting = false; GameEvents.onCollision.Add(AnalyseCollision); // Start collision detection GameEvents.onVesselCreate.Add(DebrisDelayedCleanUp); - DisableCometSpawning(); - GameEvents.onCometSpawned.Add(RemoveCometVessel); + CometCleanup(true); competitionStartTime = Planetarium.GetUniversalTime(); nextUpdateTick = competitionStartTime + 2; // 2 seconds before we start tracking decisionTick = BDArmorySettings.COMPETITION_KILLER_GM_FREQUENCY > 60 ? -1 : competitionStartTime + BDArmorySettings.COMPETITION_KILLER_GM_FREQUENCY; // every 60 seconds we do nasty things @@ -1298,10 +392,11 @@ void CompetitionStarted() Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: Competition Started"); } - public void ResetCompetitionStuff() + public void ResetCompetitionStuff(string tag = "") { // reinitilize everything when the button get hit. CompetitionID = (int)DateTime.UtcNow.Subtract(new DateTime(2020, 1, 1)).TotalSeconds; + competitionTag = tag; VesselModuleRegistry.CleanRegistries(); DoPreflightChecks(); KillTimer.Clear(); @@ -1313,16 +408,23 @@ public void ResetCompetitionStuff() if (BDArmorySettings.ASTEROID_RAIN) { AsteroidRain.Instance.Reset(); RemoveDebrisNow(); } if (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 41) BDArmorySettings.FIRE_RATE_OVERRIDE = BDArmorySettings.FIRE_RATE_OVERRIDE_CENTER; finalGracePeriodStart = -1; + competitiveTeamsAliveLimit = (BDArmorySettings.WAYPOINTS_MODE || (BDArmorySettings.RUNWAY_PROJECT && (BDArmorySettings.RUNWAY_PROJECT_ROUND == 50 || BDArmorySettings.RUNWAY_PROJECT_ROUND == 55))) ? 1 : 2; + altitudeLimitGracePeriod = BDArmorySettings.COMPETITION_INITIAL_GRACE_PERIOD; competitionPreStartTime = Planetarium.GetUniversalTime(); competitionStartTime = competitionIsActive ? Planetarium.GetUniversalTime() : -1; nextUpdateTick = competitionStartTime + 2; // 2 seconds before we start tracking decisionTick = BDArmorySettings.COMPETITION_KILLER_GM_FREQUENCY > 60 ? -1 : competitionStartTime + BDArmorySettings.COMPETITION_KILLER_GM_FREQUENCY; // every 60 seconds we do nasty things + killerGMenabled = false; FX.BulletHitFX.CleanPartsOnFireInfo(); Scores.ConfigurePlayers(GetAllPilots().Select(p => p.vessel).ToList()); // Get the competitors. + if (BDArmorySettings.RUNWAY_PROJECT && !String.IsNullOrEmpty(BDArmorySettings.PINATA_NAME) && Scores.Players.Contains(BDArmorySettings.PINATA_NAME)) { hasPinata = true; pinataAlive = false; } else { hasPinata = false; pinataAlive = false; } // Piñata. if (SpawnUtils.originalTeams.Count == 0) SpawnUtils.SaveTeams(); // If the vessels weren't spawned in with Vessel Spawner, save the current teams. + if (LoadedVesselSwitcher.Instance is not null) LoadedVesselSwitcher.Instance.ResetDeadVessels(); + dragLimiting.Clear(); + System.GC.Collect(); // Clear out garbage at a convenient time. } - IEnumerator DogfightCompetitionModeRoutine(float distance) + IEnumerator DogfightCompetitionModeRoutine(float distance, bool startDespiteFailures = false) { competitionStarting = true; competitionType = CompetitionType.FFA; @@ -1338,15 +440,33 @@ IEnumerator DogfightCompetitionModeRoutine(float distance) IBDAIControl pilot = VesselModuleRegistry.GetModule(loadedVessels.Current); if (pilot == null || !pilot.weaponManager || pilot.weaponManager.Team.Neutral) continue; + //so, for NPC on NPC violence prevention - have NPCs set to be allies of each other, or set to the same team? Should also probably have a toggle for if NPCs are friends w/ each other + + if (!String.IsNullOrEmpty(BDArmorySettings.REMOTE_ORC_NPCS_TEAM) && loadedVessels.Current.GetName().Contains(BDArmorySettings.REMOTE_ORCHESTRATION_NPC_SWAPPER)) pilot.weaponManager.SetTeam(BDTeam.Get(BDArmorySettings.REMOTE_ORC_NPCS_TEAM)); + + if (!String.IsNullOrEmpty(BDArmorySettings.PINATA_NAME) && hasPinata) + { + if (!pilot.vessel.GetName().Contains(BDArmorySettings.PINATA_NAME)) + + pilot.weaponManager.SetTeam(BDTeam.Get("PinataPoppers")); + else + { + pilot.weaponManager.SetTeam(BDTeam.Get("Pinata")); + if (FlightGlobals.ActiveVessel != pilot.vessel) + { + LoadedVesselSwitcher.Instance.ForceSwitchVessel(pilot.vessel); + } + } + } if (!pilots.TryGetValue(pilot.weaponManager.Team, out List teamPilots)) { teamPilots = new List(); pilots.Add(pilot.weaponManager.Team, teamPilots); - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: Adding Team " + pilot.weaponManager.Team.Name); + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: Adding Team " + pilot.weaponManager.Team.Name); } teamPilots.Add(pilot); - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: Adding Pilot " + pilot.vessel.GetName()); + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: Adding Pilot " + pilot.vessel.GetName()); readyToLaunch.Add(pilot); } @@ -1366,7 +486,7 @@ IEnumerator DogfightCompetitionModeRoutine(float distance) } if (!BDArmorySettings.NO_ENGINES && SpawnUtils.CountActiveEngines(pilot.vessel) == 0) // Find vessels that didn't activate their engines on AG10 and fire their next stage. { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: " + pilot.vessel.vesselName + " didn't activate engines on AG10! Activating ALL their engines."); + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: " + pilot.vessel.vesselName + " didn't activate engines on AG10! Activating ALL their engines."); SpawnUtils.ActivateAllEngines(pilot.vessel); } else if (BDArmorySettings.NO_ENGINES && SpawnUtils.CountActiveEngines(pilot.vessel) > 0) // Shutdown engines @@ -1415,6 +535,16 @@ IEnumerator DogfightCompetitionModeRoutine(float distance) var HPT = part.Current.FindModuleImplementing(); HPT.defenseMutator = (float)(1 / BDArmorySettings.HOS_DMG); } + if (BDArmorySettings.HOS_SAS) + { + if (part.Current.GetComponent() != null) + { + ModuleReactionWheel SAS; + SAS = part.Current.GetComponent(); + //if (part.Current.CrewCapacity == 0) + part.Current.RemoveModule(SAS); //don't strip reaction wheels from cockpits, as those are allowed + } + } if (BDArmorySettings.HOS_THRUST != 100) { using (var engine = VesselModuleRegistry.GetModuleEngines(pilot.vessel).GetEnumerator()) @@ -1430,6 +560,83 @@ IEnumerator DogfightCompetitionModeRoutine(float distance) { SpawnUtils.HackIntakes(pilot.vessel, true); } + if (BDArmorySettings.RUNWAY_PROJECT) + { + float torqueQuantity = 0; + int APSquantity = 0; + SpawnUtils.HackActuators(pilot.vessel, true); + + using (List.Enumerator part = pilot.vessel.Parts.GetEnumerator()) + while (part.MoveNext()) + { + if (part.Current.GetComponent() != null) + { + ModuleReactionWheel SAS; + SAS = part.Current.GetComponent(); + if (part.Current.CrewCapacity == 0 || BDArmorySettings.RUNWAY_PROJECT_ROUND == 60) + { + torqueQuantity += ((SAS.PitchTorque + SAS.RollTorque + SAS.YawTorque) / 3) * (SAS.authorityLimiter / 100); + if (torqueQuantity > (BDArmorySettings.RUNWAY_PROJECT_ROUND == 60 ? 10 : BDArmorySettings.MAX_SAS_TORQUE)) + { + float excessTorque = torqueQuantity - (BDArmorySettings.RUNWAY_PROJECT_ROUND == 60 ? 10 : BDArmorySettings.MAX_SAS_TORQUE); + SAS.authorityLimiter = 100 - Mathf.Clamp(((excessTorque / ((SAS.PitchTorque + SAS.RollTorque + SAS.YawTorque) / 3)) * 100), 0, 100); + } + } + } + if (part.Current.GetComponent() != null) + { + ModuleCommand MC; + MC = part.Current.GetComponent(); + if (part.Current.CrewCapacity == 0 && MC.minimumCrew == 0) //Drone core, nuke it + part.Current.RemoveModule(MC); + } + if (BDArmorySettings.RUNWAY_PROJECT_ROUND == 59) + { + if (part.Current.GetComponent() != null) + { + ModuleWeapon gun; + gun = part.Current.GetComponent(); + if (gun.isAPS) APSquantity++; + if (APSquantity > 4) + { + part.Current.RemoveModule(gun); + IEnumerator resource = part.Current.Resources.GetEnumerator(); + while (resource.MoveNext()) + { + if (resource.Current == null) continue; + if (resource.Current.flowState) + { + resource.Current.flowState = false; + } + } + resource.Dispose(); + } + } + } + } + if (BDArmorySettings.RUNWAY_PROJECT_ROUND == 60) + { + var nuke = pilot.vessel.rootPart.FindModuleImplementing(); + if (nuke == null) + { + nuke = (BDModuleNuke)pilot.vessel.rootPart.AddModule("BDModuleNuke"); + nuke.engineCore = true; + nuke.meltDownDuration = 15; + nuke.thermalRadius = 200; + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMOde]: Adding Nuke Module to " + pilot.vessel.GetName()); + } + BDModulePilotAI pilotAI = VesselModuleRegistry.GetModule(pilot.vessel); + if (pilotAI != null) + { + pilotAI.minAltitude = Mathf.Max(pilotAI.minAltitude, 750); + pilotAI.defaultAltitude = BDArmorySettings.VESSEL_SPAWN_ALTITUDE; + pilotAI.maxAllowedAoA = 2.5f; + pilotAI.postStallAoA = 5; + pilotAI.maxSpeed = Mathf.Min(250, pilotAI.maxSpeed); + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMOde]: Setting SpaceMode Ai settings on " + pilot.vessel.GetName()); + } + } + } } //clear target database so pilots don't attack yet @@ -1439,7 +646,7 @@ IEnumerator DogfightCompetitionModeRoutine(float distance) if (pilots.Count < 2) { - Debug.Log("[BDArmory.BDACompetitionMode" + CompetitionID.ToString() + "]: Unable to start competition mode - one or more teams is empty"); + Debug.LogWarning("[BDArmory.BDACompetitionMode" + CompetitionID.ToString() + "]: Unable to start competition mode - one or more teams is empty"); competitionStatus.Set("Competition: Failed! One or more teams is empty."); competitionStartFailureReason = CompetitionStartFailureReason.OnlyOneTeam; StopCompetition(); @@ -1447,21 +654,7 @@ IEnumerator DogfightCompetitionModeRoutine(float distance) } var leaders = new List(); - using (var pilotList = pilots.GetEnumerator()) - while (pilotList.MoveNext()) - { - if (pilotList.Current.Value == null) - { - var message = "Teams got adjusted during competition start-up, aborting."; - competitionStatus.Set("Competition: " + message); - Debug.Log("[BDArmory.BDACompetitionMode]: " + message); - competitionStartFailureReason = CompetitionStartFailureReason.OnlyOneTeam; - StopCompetition(); - yield break; - } - leaders.Add(pilotList.Current.Value[0]); - } - var leaderNames = leaders.Select(l => l.vessel.vesselName).ToList(); + var leaderNames = RefreshPilots(out pilots, out leaders, false); while (leaders.Any(leader => leader == null || leader.weaponManager == null || leader.weaponManager.wingCommander == null || leader.weaponManager.wingCommander.weaponManager == null)) { yield return new WaitForFixedUpdate(); @@ -1469,12 +662,20 @@ IEnumerator DogfightCompetitionModeRoutine(float distance) { var survivingLeaders = leaders.Where(l => l != null && l.weaponManager != null).Select(l => l.vessel.vesselName).ToList(); var missingLeaders = leaderNames.Where(l => !survivingLeaders.Contains(l)).ToList(); - var message = "A team leader disappeared during competition start-up, aborting: " + string.Join(", ", missingLeaders); - competitionStatus.Set("Competition: " + message); - Debug.Log("[BDArmory.BDACompetitionMode]: 1. " + message); - competitionStartFailureReason = CompetitionStartFailureReason.TeamLeaderDisappeared; - StopCompetition(); - yield break; + var message = "A team leader disappeared during competition start-up, " + (startDespiteFailures ? "continuing anyway" : "aborting") + ": " + string.Join(", ", missingLeaders); + Debug.LogWarning("[BDArmory.BDACompetitionMode]: " + message); + if (startDespiteFailures) + { + competitionStatus.Add("Competition: " + message); + leaderNames = RefreshPilots(out pilots, out leaders, false); + } + else + { + competitionStatus.Set("Competition: " + message); + competitionStartFailureReason = CompetitionStartFailureReason.TeamLeaderDisappeared; + StopCompetition(); + yield break; + } } } foreach (var leader in leaders) @@ -1487,17 +688,43 @@ IEnumerator DogfightCompetitionModeRoutine(float distance) { var survivingLeaders = leaders.Where(l => l != null && l.weaponManager != null).Select(l => l.vessel.vesselName).ToList(); var missingLeaders = leaderNames.Where(l => !survivingLeaders.Contains(l)).ToList(); - var message = "A team leader disappeared during competition start-up, aborting: " + string.Join(", ", missingLeaders); - competitionStatus.Set("Competition: " + message); - Debug.Log("[BDArmory.BDACompetitionMode]: 2. " + message); - competitionStartFailureReason = CompetitionStartFailureReason.TeamLeaderDisappeared; - StopCompetition(); - yield break; + var message = "A team leader disappeared during competition start-up, " + (startDespiteFailures ? "continuing anyway" : "aborting") + ": " + string.Join(", ", missingLeaders); + Debug.LogWarning("[BDArmory.BDACompetitionMode]: " + message); + if (startDespiteFailures) + { + competitionStatus.Add("Competition: " + message); + leaderNames = RefreshPilots(out pilots, out leaders, true); + } + else + { + competitionStatus.Set("Competition: " + message); + competitionStartFailureReason = CompetitionStartFailureReason.TeamLeaderDisappeared; + StopCompetition(); + yield break; + } } if (leaders.All(leader => leader.CanEngage())) { break; } + if (startCompetitionNow) + { + var readyLeaders = leaders.Where(leader => leader.CanEngage()).Select(leader => leader.vessel.vesselName).ToList(); + var message = "A team leader still isn't ready to engage and the start-now timer has run out: " + string.Join(", ", leaderNames.Where(leader => !readyLeaders.Contains(leader))); + Debug.LogWarning("[BDArmory.BDACompetitionMode]: " + message); + if (startDespiteFailures) + { + competitionStatus.Add("Competition: " + message); + break; + } + else + { + competitionStatus.Set("Competition: " + message); + competitionStartFailureReason = CompetitionStartFailureReason.Other; + StopCompetition(); + yield break; + } + } yield return new WaitForSeconds(1); } @@ -1510,14 +737,14 @@ IEnumerator DogfightCompetitionModeRoutine(float distance) while (leader.MoveNext()) center += leader.Current.vessel.CoM; center /= leaders.Count; - Vector3 startDirection = Vector3.ProjectOnPlane(leaders[0].vessel.CoM - center, VectorUtils.GetUpDirection(center)).normalized; - startDirection *= (distance * leaders.Count / 4) + 1250f; + Vector3 startDirection = (leaders[0].vessel.CoM - center).ProjectOnPlanePreNormalized(VectorUtils.GetUpDirection(center)).normalized; + startDirection *= (distance + 2 * 2000) / 2 / Mathf.Sin(Mathf.PI / leaders.Count); // 2000 is the orbiting radius of each team. Quaternion directionStep = Quaternion.AngleAxis(360f / leaders.Count, VectorUtils.GetUpDirection(center)); for (var i = 0; i < leaders.Count; ++i) { var pilotAI = VesselModuleRegistry.GetBDModulePilotAI(leaders[i].vessel, true); // Adjust initial fly-to point for terrain and default altitudes. - var startPosition = center + startDirection + (pilotAI != null ? (pilotAI.defaultAltitude - Utils.GetRadarAltitudeAtPos(center + startDirection, false)) * VectorUtils.GetUpDirection(center + startDirection) : Vector3.zero); + var startPosition = center + startDirection + (pilotAI != null ? (pilotAI.defaultAltitude - BodyUtils.GetRadarAltitudeAtPos(center + startDirection, false)) * VectorUtils.GetUpDirection(center + startDirection) : Vector3.zero); leaders[i].CommandFlyTo(VectorUtils.WorldPositionToGeoCoords(startPosition, FlightGlobals.currentMainBody)); startDirection = directionStep * startDirection; } @@ -1536,62 +763,80 @@ IEnumerator DogfightCompetitionModeRoutine(float distance) { var survivingLeaders = leaders.Where(l => l != null && l.weaponManager != null).Select(l => l.vessel.vesselName).ToList(); var missingLeaders = leaderNames.Where(l => !survivingLeaders.Contains(l)).ToList(); - var message = "A team leader disappeared during competition start-up, aborting: " + string.Join(", ", missingLeaders); - competitionStatus.Set("Competition: " + message); - Debug.Log("[BDArmory.BDACompetitionMode]: 3. " + message); - competitionStartFailureReason = CompetitionStartFailureReason.TeamLeaderDisappeared; - StopCompetition(); - yield break; + var message = "A team leader disappeared during competition start-up, " + (startDespiteFailures ? "continuing anyway" : "aborting") + ": " + string.Join(", ", missingLeaders); + Debug.LogWarning("[BDArmory.BDACompetitionMode]: " + message); + if (startDespiteFailures) + { + competitionStatus.Add("Competition: " + message); + leaderNames = RefreshPilots(out pilots, out leaders, true); + } + else + { + competitionStatus.Set("Competition: " + message); + competitionStartFailureReason = CompetitionStartFailureReason.TeamLeaderDisappeared; + StopCompetition(); + yield break; + } } - foreach (var leader in leaders) + try // Somehow, if a vessel gets destroyed during competition start, the following can throw a null reference exception despite checking for nulls! This is due to the IBDAIControl.transform getter. { - foreach (var otherLeader in leaders) + if (startDespiteFailures && pilots.Values.SelectMany(p => p).Any(p => p == null || p.weaponManager == null)) leaderNames = RefreshPilots(out pilots, out leaders, true); + foreach (var leader in leaders) { - if (leader == otherLeader) - continue; - try // Somehow, if a vessel gets destroyed during competition start, the following can throw a null reference exception despite checking for nulls! This is due to the IBDAIControl.transform getter. How to fix this? + foreach (var otherLeader in leaders) { + if (leader == otherLeader) + continue; if ((leader.transform.position - otherLeader.transform.position).sqrMagnitude < sqrDistance) waiting = true; } - catch (Exception e) + + // Increase the distance for large teams + if (!pilots.ContainsKey(leader.weaponManager.Team)) { - var message = "A team leader has disappeared during competition start-up, aborting."; - Debug.LogWarning("[BDArmory.BDACompetitionMode]: Exception thrown in DogfightCompetitionModeRoutine: " + e.Message + "\n" + e.StackTrace); - try - { - var survivingLeaders = leaders.Where(l => l != null && l.weaponManager != null).Select(l => l.vessel.vesselName).ToList(); - var missingLeaders = leaderNames.Where(l => !survivingLeaders.Contains(l)).ToList(); - message = "A team leader disappeared during competition start-up, aborting: " + string.Join(", ", missingLeaders); - } - catch (Exception e2) { Debug.LogWarning($"[BDArmory.BDACompetitionMode]: Exception gathering missing leader names:" + e2.Message); } - competitionStatus.Set(message); - Debug.Log("[BDArmory.BDACompetitionMode]: 4. " + message); - competitionStartFailureReason = CompetitionStartFailureReason.TeamLeaderDisappeared; + var message = "The teams were changed during competition start-up, aborting"; + competitionStatus.Set("Competition: " + message); + Debug.LogWarning("[BDArmory.BDACompetitionMode]: " + message); + competitionStartFailureReason = CompetitionStartFailureReason.TeamsChanged; StopCompetition(); yield break; } - } + var teamDistance = BDArmorySettings.COMPETITION_INTRA_TEAM_SEPARATION_BASE + BDArmorySettings.COMPETITION_INTRA_TEAM_SEPARATION_PER_MEMBER * pilots[leader.weaponManager.Team].Count; + foreach (var pilot in pilots[leader.weaponManager.Team]) + if (pilot != null + && pilot.currentCommand == PilotCommands.Follow + && (pilot.vessel.CoM - pilot.commandLeader.vessel.CoM).sqrMagnitude > teamDistance * teamDistance) + waiting = true; - // Increase the distance for large teams - if (!pilots.ContainsKey(leader.weaponManager.Team)) + if (waiting) break; + } + } + catch (Exception e) + { + var message = "A team leader has disappeared during competition start-up, " + (startDespiteFailures ? "continuing anyway" : "aborting"); + Debug.LogWarning("[BDArmory.BDACompetitionMode]: Exception thrown in DogfightCompetitionModeRoutine: " + e.Message + "\n" + e.StackTrace); + try { - var message = "The teams were changed during competition start-up, aborting."; - competitionStatus.Set("Competition: " + message); - Debug.Log("[BDArmory.BDACompetitionMode]: " + message); - competitionStartFailureReason = CompetitionStartFailureReason.TeamsChanged; + var survivingLeaders = leaders.Where(l => l != null && l.weaponManager != null).Select(l => l.vessel.vesselName).ToList(); + var missingLeaders = leaderNames.Where(l => !survivingLeaders.Contains(l)).ToList(); + message = "A team leader disappeared during competition start-up, " + (startDespiteFailures ? "continuing anyway" : "aborting") + ": " + string.Join(", ", missingLeaders); + } + catch (Exception e2) { Debug.LogWarning($"[BDArmory.BDACompetitionMode]: Exception gathering missing leader names:" + e2.Message); } + Debug.LogWarning("[BDArmory.BDACompetitionMode]: " + message); + if (startDespiteFailures) + { + competitionStatus.Add(message); + leaderNames = RefreshPilots(out pilots, out leaders, true); + waiting = true; + } + else + { + competitionStatus.Set(message); + competitionStartFailureReason = CompetitionStartFailureReason.TeamLeaderDisappeared; StopCompetition(); yield break; } - var teamDistance = 800f + 100f * pilots[leader.weaponManager.Team].Count; - foreach (var pilot in pilots[leader.weaponManager.Team]) - if (pilot != null - && pilot.currentCommand == PilotCommands.Follow - && (pilot.vessel.CoM - pilot.commandLeader.vessel.CoM).sqrMagnitude > teamDistance * teamDistance) - waiting = true; - - if (waiting) break; } yield return null; @@ -1599,13 +844,14 @@ IEnumerator DogfightCompetitionModeRoutine(float distance) previousNumberCompetitive = 2; // For entering into tag mode //start the match + if (startDespiteFailures && pilots.Values.SelectMany(p => p).Any(p => p == null || p.weaponManager == null)) leaderNames = RefreshPilots(out pilots, out leaders, true); foreach (var teamPilots in pilots.Values) { if (teamPilots == null) { - var message = "Teams have been changed during competition start-up, aborting."; + var message = "Teams have been changed during competition start-up, aborting"; competitionStatus.Set("Competition: " + message); - Debug.Log("[BDArmory.BDACompetitionMode]: " + message); + Debug.LogWarning("[BDArmory.BDACompetitionMode]: " + message); competitionStartFailureReason = CompetitionStartFailureReason.TeamsChanged; StopCompetition(); yield break; @@ -1613,9 +859,9 @@ IEnumerator DogfightCompetitionModeRoutine(float distance) foreach (var pilot in teamPilots) if (pilot == null) { - var message = "A pilot has disappeared from team during competition start-up, aborting."; + var message = "A pilot has disappeared from team during competition start-up, aborting"; competitionStatus.Set("Competition: " + message); - Debug.Log("[BDArmory.BDACompetitionMode]: " + message); + Debug.LogWarning("[BDArmory.BDACompetitionMode]: " + message); competitionStartFailureReason = CompetitionStartFailureReason.PilotDisappeared; StopCompetition(); // Check that the team pilots haven't been changed during the competition startup. yield break; @@ -1629,11 +875,18 @@ IEnumerator DogfightCompetitionModeRoutine(float distance) } catch (Exception e) { - Debug.LogError($"[BDArmory.BDACompetitionMode]: Exception thrown in DogfightCompetitionModeRoutine: " + e.Message); - competitionStatus.Set("Failed to update radar cross sections, aborting."); - competitionStartFailureReason = CompetitionStartFailureReason.Other; - StopCompetition(); - yield break; + Debug.LogError($"[BDArmory.BDACompetitionMode]: Exception thrown in DogfightCompetitionModeRoutine: " + e.Message + "\n" + e.StackTrace); + if (startDespiteFailures) + { + competitionStatus.Add("Failed to update radar cross sections, continuing anyway"); + } + else + { + competitionStatus.Set("Failed to update radar cross sections, aborting"); + competitionStartFailureReason = CompetitionStartFailureReason.Other; + StopCompetition(); + yield break; + } } } foreach (var teamPilots in pilots) @@ -1644,8 +897,8 @@ IEnumerator DogfightCompetitionModeRoutine(float distance) if (!pilot.weaponManager.guardMode) pilot.weaponManager.ToggleGuardMode(); - foreach (var leader in leaders) - BDATargetManager.ReportVessel(pilot.vessel, leader.weaponManager); + //foreach (var leader in leaders) + //BDATargetManager.ReportVessel(pilot.vessel, leader.weaponManager); pilot.ReleaseCommand(); pilot.CommandAttack(centerGPS); @@ -1688,22 +941,45 @@ public List GetAllPilots() return pilots; } + /// + /// Refresh the pilots and leaders after a team change or vessel breaks or disappears. + /// Note: team changes don't always seem to trigger this, but vessel loss does. + /// + /// + /// + /// + /// + List RefreshPilots(out Dictionary> pilots, out List leaders, bool followLeaders) + { + var allPilots = GetAllPilots(); + var teams = allPilots.Select(p => p.weaponManager.Team).ToHashSet(); // Unique list + pilots = teams.ToDictionary(t => t, t => allPilots.Where(p => p.weaponManager.Team == t).ToList()); + leaders = pilots.Select(kvp => kvp.Value.First()).ToList(); + if (followLeaders) + { + foreach (var leader in leaders) + if (leader.currentCommand != PilotCommands.Free) + leader.weaponManager.wingCommander.CommandAllFollow(); + } + return leaders.Select(l => l.vessel.vesselName).ToList(); + } + public string currentMutator; public void ConfigureMutator() { currentMutator = string.Empty; - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: MutatorMode enabled; Mutator count = " + BDArmorySettings.MUTATOR_LIST.Count); + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: MutatorMode enabled; Mutator count = " + BDArmorySettings.MUTATOR_LIST.Count); var indices = Enumerable.Range(0, BDArmorySettings.MUTATOR_LIST.Count).ToList(); indices.Shuffle(); currentMutator = string.Join("; ", indices.Take(BDArmorySettings.MUTATOR_APPLY_NUM).Select(i => MutatorInfo.mutators[BDArmorySettings.MUTATOR_LIST[i]].name)); - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: current mutators: " + currentMutator); + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: current mutators: " + currentMutator); MutatorResetTime = Planetarium.GetUniversalTime(); if (BDArmorySettings.MUTATOR_APPLY_GLOBAL) //selected mutator applied globally { - ScreenMessages.PostScreenMessage(Localizer.Format("#LOC_BDArmory_UI_MutatorStart") + ": " + currentMutator + ". " + (BDArmorySettings.MUTATOR_APPLY_TIMER ? (BDArmorySettings.MUTATOR_DURATION > 0 ? BDArmorySettings.MUTATOR_DURATION * 60 : BDArmorySettings.COMPETITION_DURATION * 60) + " seconds left" : ""), 5, ScreenMessageStyle.UPPER_CENTER); + ScreenMessages.PostScreenMessage(StringUtils.Localize("#LOC_BDArmory_UI_MutatorStart") + ": " + currentMutator + ". " + (BDArmorySettings.MUTATOR_APPLY_TIMER ? (BDArmorySettings.MUTATOR_DURATION > 0 ? BDArmorySettings.MUTATOR_DURATION * 60 : BDArmorySettings.COMPETITION_DURATION * 60) + " seconds left" : ""), 5, ScreenMessageStyle.UPPER_CENTER); } } @@ -1726,7 +1002,7 @@ public InvalidVesselReason IsValidVessel(Vessel vessel, bool attemptFix = true) void OnCrewOnEVA(GameEvents.FromToAction fromToAction) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log($"[BDArmory.BDACompetitionMode]: {fromToAction.to} went on EVA from {fromToAction.from}"); + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log($"[BDArmory.BDACompetitionMode]: {fromToAction.to} went on EVA from {fromToAction.from}"); if (fromToAction.from.vessel != null) { OnVesselModified(fromToAction.from.vessel); @@ -1778,7 +1054,7 @@ public void CheckForAutonomousCombatSeat(Vessel vessel) { if (vessel.parts.Count == 1) // Check for a falling combat seat. { - Debug.Log("[BDArmory.BDACompetitionMode]: Found a lone combat seat, killing it."); + Debug.Log($"[BDArmory.BDACompetitionMode]: Found a lone combat seat ({vessel.vesselName}), killing it."); PartExploderSystem.AddPartToExplode(vessel.parts[0]); return; } @@ -1788,7 +1064,7 @@ public void CheckForAutonomousCombatSeat(Vessel vessel) { if (VesselModuleRegistry.GetModules(vessel).All(c => c.GetControlSourceState() == CommNet.VesselControlState.None)) { - Debug.Log("[BDArmory.BDACompetitionMode]: Kerbal has left the seat of " + vessel.vesselName + " and it has no other controls, disabling the AI."); + Debug.Log($"[BDArmory.BDACompetitionMode]: Kerbal has left the seat of {vessel.vesselName} and it has no other controls, disabling the AI."); AI.DeactivatePilot(); } } @@ -1822,7 +1098,7 @@ IEnumerator DelayedExplodeWMs(Vessel vessel, float delay = 1f, UncontrolledReaso { if (explodingWM.Contains(vessel)) yield break; // Already scheduled for exploding. explodingWM.Add(vessel); - yield return new WaitForSeconds(delay); + yield return new WaitForSecondsFixed(delay); if (vessel == null) // It's already dead. { explodingWM = explodingWM.Where(v => v != null).ToHashSet(); // Clean the hashset. @@ -1868,16 +1144,17 @@ void CheckForBadlyNamedVessels() #region Runway Project public bool killerGMenabled = false; + public bool hasPinata = false; public bool pinataAlive = false; public bool s4r1FiringRateUpdatedFromShotThisFrame = false; public bool s4r1FiringRateUpdatedFromHitThisFrame = false; - public void StartRapidDeployment(float distance) + public void StartRapidDeployment(float distance, string tag = "") { if (!BDArmorySettings.RUNWAY_PROJECT) return; if (!sequencedCompetitionStarting) { - ResetCompetitionStuff(); + ResetCompetitionStuff(tag); Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: Starting Rapid Deployment "); RemoveDebrisNow(); GameEvents.onVesselPartCountChanged.Add(OnVesselModified); @@ -1889,7 +1166,7 @@ public void StartRapidDeployment(float distance) List commandSequence; switch (BDArmorySettings.RUNWAY_PROJECT_ROUND) { - case 33: + case 33: //S1R7/S3R3 Rapid deployment I/II commandSequence = new List{ "0:MassTrim", // t=0, mass trim "0:ActionGroup:14:0", // t=0, Disable brakes @@ -1934,16 +1211,38 @@ public void StartRapidDeployment(float distance) "5:ToggleGuard:1", // t=65, Enable guard mode }; break; - case 60: //change this later (orbital deployment) + case 53: //change this later (orbital deployment) commandSequence = new List{ "0:ActionGroup:13:1", // t=0, AG4 - Enable SAS "0:ActionGroup:16:0", // t=0, Retract gear (if it's not retracted) - "0:HackGravity:10", // t=0, Increase gravity to 10x - "30:HackGravity:1", //t=30, Reset gravity + "0:ActionGroup:14:0", // t=0, Disable brakes "0:ActionGroup:10", // t=30, AG10 "0:ActivateEngines", // t=30, Activate engines + "0:HackGravity:10", // t=0, Increase gravity to 10x + "0:TimeScale:2", // t=0, scale time for faster falling + "0:ToggleGuard:0", // t=0, Disable guard mode (for those who triggered it early) + "0:TogglePilot:0", // t=0, Disable pilots (for those who triggered it early) + "30:HackGravity:1", //t=30, Reset gravity + "0:TimeScale:1", // t=0, reset time scaling "0:SetThrottle:100", // t=30, Full throttle "0:TogglePilot:1", // t=30, Activate pilots + "0:AttackCenter", // t=30, "Attack" center point + "0:ToggleGuard:53", // t=30+, Activate guard mode (attack) (delayed) + "0:RemoveDebris", // t=30, Remove any other debris and spectators + "0:ActivateCompetition", // t=30, mark the competition as active + // "30:EnableGM", // t=60, Activate the killer GM + }; + altitudeLimitGracePeriod = 30; // t=60 (30s after the competition starts), activate the altitude limit + break; + case 60: //change this later (Pinata deployment) + commandSequence = new List{ + "0:ActionGroup:13:1", // t=0, AG4 - Enable SAS + "0:ActionGroup:16:0", // t=0, Retract gear (if it's not retracted) + "0:ActionGroup:10", // t=0, AG10 + "0:ActivateEngines", // t=0, Activate engines + "0:SetThrottle:100", // t=0, Full throttle + "0:TogglePilot:1", // t=30, Activate pilots + "0:SetTeam:1", //t=0, Set everyone to same team "0:ToggleGuard:1", // t=30, Activate guard mode (attack) "5:RemoveDebris", // t=35, Remove any other debris and spectators // "0:EnableGM", // t=60, Activate the killer GM @@ -2132,8 +1431,8 @@ private void DoRapidDeploymentMassTrim(float targetMass = 65f) } var mass = pilot.vessel.GetTotalMass(); - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: UNSHIELDED:" + notShieldedCount.ToString() + ":" + pilot.vessel.GetName()); - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: MASS:" + mass.ToString() + ":" + pilot.vessel.GetName()); + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: UNSHIELDED:" + notShieldedCount.ToString() + ":" + pilot.vessel.GetName()); + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: MASS:" + mass.ToString() + ":" + pilot.vessel.GetName()); if (mass < lowestMass) { lowestMass = mass; @@ -2168,7 +1467,7 @@ private void DoRapidDeploymentMassTrim(float targetMass = 65f) if (oreAmount > 1500) oreAmount = 1500; resources.Current.amount = oreAmount; } - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: RESOURCEUPDATE:" + pilot.vessel.GetName() + ":" + resources.Current.amount); + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: RESOURCEUPDATE:" + pilot.vessel.GetName() + ":" + resources.Current.amount); massAdded = true; } } @@ -2179,15 +1478,7 @@ private void DoRapidDeploymentMassTrim(float targetMass = 65f) IEnumerator SequencedCompetition(List commandSequence) { - var pilots = GetAllPilots(); - if (pilots.Count < 2) - { - Debug.Log("[BDArmory.BDACompetitionMode" + CompetitionID.ToString() + "]: Unable to start sequenced competition - one or more teams is empty"); - competitionStatus.Set("Competition: Failed! One or more teams is empty."); - competitionStartFailureReason = CompetitionStartFailureReason.OnlyOneTeam; - StopCompetition(); - yield break; - } + var pilots = GetAllPilots(); // We don't check the number of pilots here so that the sequence can be done with a single pilot. Instead, we check later before actually starting the competition. sequencedCompetitionStarting = true; competitionType = CompetitionType.SEQUENCED; double startTime = Planetarium.GetUniversalTime(); @@ -2210,10 +1501,9 @@ IEnumerator SequencedCompetition(List commandSequence) } var timeStep = int.Parse(parts[0]); nextStep = Planetarium.GetUniversalTime() + timeStep; - while (Planetarium.GetUniversalTime() < nextStep) - { - yield return new WaitForFixedUpdate(); - } + yield return new WaitWhile(() => (Planetarium.GetUniversalTime() < nextStep)); + + pilots = pilots.Where(pilot => pilot != null && pilot.vessel != null && gameObject != null).ToList(); // Clear out any dead pilots. (Apparently we also need to check the gameObject!) var command = parts[1]; @@ -2221,11 +1511,11 @@ IEnumerator SequencedCompetition(List commandSequence) { case "Stage": { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: Staging."); + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: Staging."); // activate stage foreach (var pilot in pilots) { - Utils.fireNextNonEmptyStage(pilot.vessel); + VesselUtils.fireNextNonEmptyStage(pilot.vessel); } break; } @@ -2233,11 +1523,11 @@ IEnumerator SequencedCompetition(List commandSequence) { if (parts.Count() < 3 || parts.Count() > 4) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: Competition Command not parsed correctly " + cmdEvent); + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: Competition Command not parsed correctly " + cmdEvent); StopCompetition(); yield break; } - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: Jiggling action group " + parts[2] + "."); + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: Jiggling action group " + parts[2] + "."); foreach (var pilot in pilots) { if (parts.Count() == 3) @@ -2258,7 +1548,7 @@ IEnumerator SequencedCompetition(List commandSequence) } case "TogglePilot": { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: Toggling autopilot."); + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: Toggling autopilot."); if (parts.Count() == 3) { var newState = true; @@ -2285,24 +1575,35 @@ IEnumerator SequencedCompetition(List commandSequence) } case "ToggleGuard": { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: Toggling guard mode."); + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: Toggling guard mode."); if (parts.Count() == 3) { - var newState = true; - if (parts[2] == "0") - { - newState = false; - } - foreach (var pilot in pilots) + switch (parts[2]) { - if (pilot.weaponManager != null && pilot.weaponManager.guardMode != newState) - { - pilot.weaponManager.ToggleGuardMode(); - if (!pilot.weaponManager.guardMode) pilot.weaponManager.SetTarget(null); - } + case "0": + case "1": + var newState = true; + if (parts[2] == "0") + { + newState = false; + } + foreach (var pilot in pilots) + { + if (pilot.weaponManager != null && pilot.weaponManager.guardMode != newState) + { + pilot.weaponManager.ToggleGuardMode(); + if (!pilot.weaponManager.guardMode) pilot.weaponManager.SetTarget(null); + } + } + break; + case "53": // Orbital deployment + var limit = (BDArmorySettings.COMPETITION_ALTITUDE_LIMIT_HIGH < 20f ? BDArmorySettings.COMPETITION_ALTITUDE_LIMIT_HIGH / 10f : BDArmorySettings.COMPETITION_ALTITUDE_LIMIT_HIGH < 39f ? BDArmorySettings.COMPETITION_ALTITUDE_LIMIT_HIGH - 18f : (BDArmorySettings.COMPETITION_ALTITUDE_LIMIT_HIGH - 38f) * 5f + 20f) * 1000f; + foreach (var pilot in pilots) + StartCoroutine(EnableGuardModeWhen(pilot, () => (pilot == null || pilot.vessel == null || pilot.vessel.radarAltitude < limit))); + break; } } - else + else // FIXME This branch isn't taken as all the ToggleGuard commands have 3 parts. { foreach (var pilot in pilots) { @@ -2327,6 +1628,62 @@ IEnumerator SequencedCompetition(List commandSequence) MM.EnableMutator(); //random mutator } } + if (BDArmorySettings.RUNWAY_PROJECT) + { + float torqueQuantity = 0; + int APSquantity = 0; + SpawnUtils.HackActuators(pilot.vessel, true); + + using (List.Enumerator part = pilot.vessel.Parts.GetEnumerator()) + while (part.MoveNext()) + { + if (part.Current.GetComponent() != null) + { + ModuleReactionWheel SAS; + SAS = part.Current.GetComponent(); + if (part.Current.CrewCapacity == 0) + { + torqueQuantity += ((SAS.PitchTorque + SAS.RollTorque + SAS.YawTorque) / 3) * (SAS.authorityLimiter / 100); + if (torqueQuantity > BDArmorySettings.MAX_SAS_TORQUE) + { + float excessTorque = torqueQuantity - BDArmorySettings.MAX_SAS_TORQUE; + SAS.authorityLimiter = 100 - Mathf.Clamp(((excessTorque / ((SAS.PitchTorque + SAS.RollTorque + SAS.YawTorque) / 3)) * 100), 0, 100); + } + } + } + if (part.Current.GetComponent() != null) + { + ModuleCommand MC; + MC = part.Current.GetComponent(); + if (part.Current.CrewCapacity == 0 && MC.minimumCrew == 0) //Drone core, nuke it + part.Current.RemoveModule(MC); + } + if (BDArmorySettings.RUNWAY_PROJECT_ROUND == 59) + { + if (part.Current.GetComponent() != null) + { + ModuleWeapon gun; + gun = part.Current.GetComponent(); + if (gun.isAPS) APSquantity++; + if (APSquantity > 4) + { + part.Current.RemoveModule(gun); + IEnumerator resource = part.Current.Resources.GetEnumerator(); + while (resource.MoveNext()) + { + if (resource.Current == null) continue; + if (resource.Current.flowState) + { + resource.Current.flowState = false; + } + } + resource.Dispose(); + } + } + } + } + } + if (BDArmorySettings.ENABLE_HOS && BDArmorySettings.HALL_OF_SHAME_LIST.Count > 0) { if (BDArmorySettings.HALL_OF_SHAME_LIST.Contains(pilot.vessel.GetName())) @@ -2353,19 +1710,56 @@ IEnumerator SequencedCompetition(List commandSequence) var HPT = part.Current.FindModuleImplementing(); HPT.defenseMutator = (float)(1 / BDArmorySettings.HOS_DMG); } + if (BDArmorySettings.HOS_SAS) + { + if (part.Current.GetComponent() != null) + { + ModuleReactionWheel SAS; + SAS = part.Current.GetComponent(); + part.Current.RemoveModule(SAS); + } + } } } } } } - + break; + } + case "AttackCenter": + { + Vector3 center = Vector3.zero; + foreach (var pilot in pilots) center += pilot.vessel.CoM; + center /= pilots.Count; + Vector3 centerGPS = VectorUtils.WorldPositionToGeoCoords(center, FlightGlobals.currentMainBody); + centerGPS.z = (float)BodyUtils.GetTerrainAltitudeAtPos(center) + 1000; // Target 1km above the terrain at the center. + foreach (var pilot in pilots) + { + pilot.ReleaseCommand(); + pilot.CommandAttack(centerGPS); + } + break; + } + case "SetTeam": + { + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: setting team."); + foreach (var pilot in pilots) + { + if (!String.IsNullOrEmpty(BDArmorySettings.PINATA_NAME) && hasPinata) + { + if (!pilot.vessel.GetName().Contains(BDArmorySettings.PINATA_NAME)) + pilot.weaponManager.SetTeam(BDTeam.Get("PinataPoppers")); + else + pilot.weaponManager.SetTeam(BDTeam.Get("Pinata")); + } + } break; } case "SetThrottle": { - if (parts.Count() == 3) + if (parts.Count() == 3 && pilots.Count > 1) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: Adjusting throttle to " + parts[2] + "%."); + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: Adjusting throttle to " + parts[2] + "%."); var someOtherVessel = pilots[0].vessel == FlightGlobals.ActiveVessel ? pilots[1].vessel : pilots[0].vessel; foreach (var pilot in pilots) { @@ -2381,7 +1775,7 @@ IEnumerator SequencedCompetition(List commandSequence) } case "RemoveDebris": { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: Removing debris and non-competitors."); + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: Removing debris and non-competitors."); // remove anything that doesn't contain BD Armory modules RemoveNonCompetitors(true); RemoveDebrisNow(); @@ -2389,7 +1783,7 @@ IEnumerator SequencedCompetition(List commandSequence) } case "RemoveFairings": { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: Removing fairings."); + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: Removing fairings."); // removes the fairings after deplyment to stop the physical objects consuming CPU var rmObj = new List(); foreach (var phyObj in FlightGlobals.physicalObjects) @@ -2404,7 +1798,7 @@ IEnumerator SequencedCompetition(List commandSequence) } case "EnableGM": { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: Activating killer GM."); + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: Activating killer GM."); killerGMenabled = true; decisionTick = BDArmorySettings.COMPETITION_KILLER_GM_FREQUENCY > 60 ? -1 : Planetarium.GetUniversalTime() + BDArmorySettings.COMPETITION_KILLER_GM_FREQUENCY; ResetSpeeds(); @@ -2412,12 +1806,12 @@ IEnumerator SequencedCompetition(List commandSequence) } case "ActivateEngines": { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: Activating engines."); + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: Activating engines."); foreach (var pilot in pilots) { if (!BDArmorySettings.NO_ENGINES && SpawnUtils.CountActiveEngines(pilot.vessel) == 0) // If the vessel didn't activate their engines on AG10, then activate all their engines and hope for the best. { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: " + pilot.vessel.GetName() + " didn't activate engines on AG10! Activating ALL their engines."); + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: " + pilot.vessel.GetName() + " didn't activate engines on AG10! Activating ALL their engines."); SpawnUtils.ActivateAllEngines(pilot.vessel); } else if (BDArmorySettings.NO_ENGINES && SpawnUtils.CountActiveEngines(pilot.vessel) > 0) // Shutdown engines @@ -2447,7 +1841,7 @@ IEnumerator SequencedCompetition(List commandSequence) } case "MassTrim": { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: Performing mass trim."); + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: Performing mass trim."); DoRapidDeploymentMassTrim(); break; } @@ -2455,24 +1849,53 @@ IEnumerator SequencedCompetition(List commandSequence) { if (parts.Count() == 3) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: Adjusting gravity to " + parts[2] + "x."); + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: Adjusting gravity to " + parts[2] + "x."); double grav = double.Parse(parts[2]); PhysicsGlobals.GraviticForceMultiplier = grav; VehiclePhysics.Gravity.Refresh(); - //competitionStatus.Add("Competition: Adjusting gravity to " + grav.ToString("0.0") + "G!"); + competitionStatus.Add("Competition: Adjusting gravity to " + grav.ToString("0.0") + "G!"); + } + break; + } + case "ActivateCompetition": + { + if (!competitionIsActive && pilots.Count > 1) + { + competitionStatus.Add("Competition starting! Good luck!"); + CompetitionStarted(); } break; } + case "TimeScale": + { + if (parts.Count() == 3) + Time.timeScale = float.Parse(parts[2]); + else + Time.timeScale = 1f; + break; + } default: { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: Unknown sequenced command: " + command + "."); + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: Unknown sequenced command: " + command + "."); StopCompetition(); yield break; } } } // will need a terminator routine - CompetitionStarted(); + if (pilots.Count < 2) + { + Debug.Log("[BDArmory.BDACompetitionMode" + CompetitionID.ToString() + "]: Unable to start sequenced competition - one or more teams is empty"); + competitionStatus.Set("Competition: Failed! One or more teams is empty."); + competitionStartFailureReason = CompetitionStartFailureReason.OnlyOneTeam; + StopCompetition(); + yield break; + } + if (!competitionIsActive) + { + competitionStatus.Add("Competition starting! Good luck!"); + CompetitionStarted(); + } } // ask the GM to find a 'victim' which means a slow pilot who's not shooting very much @@ -2519,7 +1942,7 @@ private void FindVictim() bool vesselNotFired = (Planetarium.GetUniversalTime() - vData.lastFiredTime) > 120; // if you can't shoot in 2 minutes you're at the front of line - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: Victim Check " + vesselName + " " + averageSpeed.ToString() + " " + vesselNotFired.ToString()); + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: Victim Check " + vesselName + " " + averageSpeed.ToString() + " " + vesselNotFired.ToString()); if (hasFired) { if (vesselNotFired) @@ -2556,21 +1979,26 @@ private void FindVictim() } Scores.RegisterDeath(vesselName, GMKillReason.GM); competitionStatus.Add(vesselName + " was killed by the GM for being too slow."); - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: GM killing " + vesselName + " for being too slow."); - Utils.ForceDeadVessel(worstVessel); + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: GM killing " + vesselName + " for being too slow."); + VesselUtils.ForceDeadVessel(worstVessel); } ResetSpeeds(); } - private void CheckAltitudeLimits() + private void CheckAltitudeLimits() //have ths start a timer if alt exceeded, instead of immediately kill? Timing/kill elements would need to be moved to MissileFire, but doable. { if (BDArmorySettings.COMPETITION_ALTITUDE_LIMIT_HIGH < 55f) // Kill off those flying too high. { var limit = (BDArmorySettings.COMPETITION_ALTITUDE_LIMIT_HIGH < 20f ? BDArmorySettings.COMPETITION_ALTITUDE_LIMIT_HIGH / 10f : BDArmorySettings.COMPETITION_ALTITUDE_LIMIT_HIGH < 39f ? BDArmorySettings.COMPETITION_ALTITUDE_LIMIT_HIGH - 18f : (BDArmorySettings.COMPETITION_ALTITUDE_LIMIT_HIGH - 38f) * 5f + 20f) * 1000f; foreach (var weaponManager in LoadedVesselSwitcher.Instance.WeaponManagers.SelectMany(tm => tm.Value).ToList()) { - if (alive.Contains(weaponManager.vessel.vesselName) && weaponManager.vessel.radarAltitude > limit) + if (alive.Contains(weaponManager.vessel.vesselName) && BDArmorySettings.COMPETITION_ALTITUDE__LIMIT_ASL ? weaponManager.vessel.altitude > limit : weaponManager.vessel.radarAltitude > limit) { + if (Scores.ScoreData[weaponManager.vessel.vesselName].AltitudeKillTimer == 0) + { + Scores.ScoreData[weaponManager.vessel.vesselName].AltitudeKillTimer = Planetarium.GetUniversalTime(); ; + } + /* var killerName = Scores.ScoreData[weaponManager.vessel.vesselName].lastPersonWhoDamagedMe; if (killerName == "") { @@ -2579,25 +2007,49 @@ private void CheckAltitudeLimits() } Scores.RegisterDeath(weaponManager.vessel.vesselName, GMKillReason.GM); competitionStatus.Add(weaponManager.vessel.vesselName + " flew too high!"); - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: " + weaponManager.vessel.vesselName + ":REMOVED:" + killerName); + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: " + weaponManager.vessel.vesselName + ":REMOVED:" + killerName); if (KillTimer.ContainsKey(weaponManager.vessel.vesselName)) KillTimer.Remove(weaponManager.vessel.vesselName); - Utils.ForceDeadVessel(weaponManager.vessel); + VesselUtils.ForceDeadVessel(weaponManager.vessel); + */ + } + else + { + if (Scores.ScoreData[weaponManager.vessel.vesselName].AltitudeKillTimer != 0) + { + // safely below ceiling for 15 seconds + if (Planetarium.GetUniversalTime() - Scores.ScoreData[weaponManager.vessel.vesselName].AltitudeKillTimer > 0) + { + Scores.ScoreData[weaponManager.vessel.vesselName].AltitudeKillTimer = 0; + } + } } } } - if (BDArmorySettings.COMPETITION_ALTITUDE_LIMIT_LOW > -39f) // Kill off those flying too low. + if (BDArmorySettings.COMPETITION_ALTITUDE_LIMIT_LOW > -39f || BDArmorySettings.ALTITUDE_HACKS) // Kill off those flying too low. { float limit; - if (BDArmorySettings.COMPETITION_ALTITUDE_LIMIT_LOW < -28f) limit = (BDArmorySettings.COMPETITION_ALTITUDE_LIMIT_LOW + 28f) * 1000f; // -10km — -1km @ 1km - else if (BDArmorySettings.COMPETITION_ALTITUDE_LIMIT_LOW < -19f) limit = (BDArmorySettings.COMPETITION_ALTITUDE_LIMIT_LOW + 19f) * 100f; // -900m — -100m @ 100m - else if (BDArmorySettings.COMPETITION_ALTITUDE_LIMIT_LOW < 0f) limit = BDArmorySettings.COMPETITION_ALTITUDE_LIMIT_LOW * 5f; // -95m — -5m @ 5m - else if (BDArmorySettings.COMPETITION_ALTITUDE_LIMIT_LOW < 20f) limit = BDArmorySettings.COMPETITION_ALTITUDE_LIMIT_LOW * 100f; // 0m — 1900m @ 100m - else if (BDArmorySettings.COMPETITION_ALTITUDE_LIMIT_LOW < 39f) limit = (BDArmorySettings.COMPETITION_ALTITUDE_LIMIT_LOW - 18f) * 1000f; // 2km — 20km @ 1km - else limit = ((BDArmorySettings.COMPETITION_ALTITUDE_LIMIT_LOW - 38f) * 5f + 20f) * 1000f; // 25km — 50km @ 5km + if (BDArmorySettings.ALTITUDE_HACKS) + { + limit = MinAlt; + } + else + { + if (BDArmorySettings.COMPETITION_ALTITUDE_LIMIT_LOW < -28f) limit = (BDArmorySettings.COMPETITION_ALTITUDE_LIMIT_LOW + 28f) * 1000f; // -10km — -1km @ 1km + else if (BDArmorySettings.COMPETITION_ALTITUDE_LIMIT_LOW < -19f) limit = (BDArmorySettings.COMPETITION_ALTITUDE_LIMIT_LOW + 19f) * 100f; // -900m — -100m @ 100m + else if (BDArmorySettings.COMPETITION_ALTITUDE_LIMIT_LOW < 0f) limit = BDArmorySettings.COMPETITION_ALTITUDE_LIMIT_LOW * 5f; // -95m — -5m @ 5m + else if (BDArmorySettings.COMPETITION_ALTITUDE_LIMIT_LOW < 20f) limit = BDArmorySettings.COMPETITION_ALTITUDE_LIMIT_LOW * 100f; // 0m — 1900m @ 100m + else if (BDArmorySettings.COMPETITION_ALTITUDE_LIMIT_LOW < 39f) limit = (BDArmorySettings.COMPETITION_ALTITUDE_LIMIT_LOW - 18f) * 1000f; // 2km — 20km @ 1km + else limit = ((BDArmorySettings.COMPETITION_ALTITUDE_LIMIT_LOW - 38f) * 5f + 20f) * 1000f; // 25km — 50km @ 5km + } foreach (var weaponManager in LoadedVesselSwitcher.Instance.WeaponManagers.SelectMany(tm => tm.Value).ToList()) { - if (alive.Contains(weaponManager.vessel.vesselName) && weaponManager.vessel.radarAltitude < limit) + if (alive.Contains(weaponManager.vessel.vesselName) && BDArmorySettings.COMPETITION_ALTITUDE__LIMIT_ASL ? weaponManager.vessel.altitude < limit : weaponManager.vessel.radarAltitude < limit) { + if (Scores.ScoreData[weaponManager.vessel.vesselName].AltitudeKillTimer == 0) + { + Scores.ScoreData[weaponManager.vessel.vesselName].AltitudeKillTimer = Planetarium.GetUniversalTime(); ; + } + /* var killerName = Scores.ScoreData[weaponManager.vessel.vesselName].lastPersonWhoDamagedMe; if (killerName == "") { @@ -2606,9 +2058,22 @@ private void CheckAltitudeLimits() } Scores.RegisterDeath(weaponManager.vessel.vesselName, GMKillReason.GM); competitionStatus.Add(weaponManager.vessel.vesselName + " flew too low!"); - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: " + weaponManager.vessel.vesselName + ":REMOVED:" + killerName); + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: " + weaponManager.vessel.vesselName + ":REMOVED:" + killerName); if (KillTimer.ContainsKey(weaponManager.vessel.vesselName)) KillTimer.Remove(weaponManager.vessel.vesselName); - Utils.ForceDeadVessel(weaponManager.vessel); + VesselUtils.ForceDeadVessel(weaponManager.vessel); + */ + } + else + { + + if (Scores.ScoreData[weaponManager.vessel.vesselName].AltitudeKillTimer != 0) + { + // safely below ceiling for 15 seconds + if (Planetarium.GetUniversalTime() - Scores.ScoreData[weaponManager.vessel.vesselName].AltitudeKillTimer > 0) + { + Scores.ScoreData[weaponManager.vessel.vesselName].AltitudeKillTimer = 0; + } + } } } } @@ -2616,7 +2081,7 @@ private void CheckAltitudeLimits() // reset all the tracked speeds, and copy the shot clock over, because I wanted 2 minutes of shooting to count private void ResetSpeeds() { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "] resetting kill clock"); + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "] resetting kill clock"); foreach (var player in Scores.Players) { if (Scores.ScoreData[player].averageCount == 0) @@ -2634,6 +2099,111 @@ private void ResetSpeeds() } } + List craftToCull = new List(); + void CullSlowWaypointRunners(double threshold) + { + var now = Planetarium.GetUniversalTime(); + craftToCull.Clear(); + foreach (var weaponManager in LoadedVesselSwitcher.Instance.WeaponManagers.SelectMany(tm => tm.Value).ToList()) + { + if (weaponManager == null || weaponManager.vessel == null) continue; + if (weaponManager.AI != null && !((BDGenericAIBase)weaponManager.AI).IsRunningWaypoints) continue; + var player = weaponManager.vessel.vesselName; + if (!Scores.Players.Contains(player)) continue; + if (Scores.ScoreData[player].waypointsReached.Count == 0) // Hasn't reached the first waypoint. + { + if (now - competitionStartTime > threshold) + { + // Debug.Log($"DEBUG Culling {player} due to {now - competitionStartTime}s since competition start and no waypoint reached. now: {now}, comp. start time: {competitionStartTime}"); + craftToCull.Add(weaponManager); + } + } + else if (now - competitionStartTime - Scores.ScoreData[player].waypointsReached.Last().timestamp > threshold) + { + // Debug.Log($"DEBUG Culling {player} due to {now - competitionStartTime - Scores.ScoreData[player].waypointsReached.Last().timestamp}s since last waypoint reached, now: {now}, last: {Scores.ScoreData[player].waypointsReached.Last().timestamp}, WP passed: {Scores.ScoreData[player].waypointsReached.Count}, comp. start time: {competitionStartTime}"); + craftToCull.Add(weaponManager); + } + } + foreach (var weaponManager in craftToCull) + { + var vesselName = weaponManager.vessel.vesselName; + Scores.ScoreData[vesselName].lastPersonWhoDamagedMe = $"Failed to reach a waypoint within {threshold:0}s"; + Scores.RegisterDeath(vesselName, GMKillReason.BigRedButton); // Mark it as a Big Red Button GM kill. + var message = $"{vesselName} failed to reach a waypoint within {threshold:0}s, killing it."; + if (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 55) message = $"{vesselName} failed to reach a waypoint within {threshold:0}s and was killed by a Tusken Raider."; + competitionStatus.Add(message); + Debug.Log($"[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: " + message); + VesselUtils.ForceDeadVessel(weaponManager.vessel); + } + craftToCull.Clear(); + } + + /// + /// Delay enabling guard mode until the condition is satisfied. + /// + /// The pilot to enable guard mode for + /// The condition to satisfy first + IEnumerator EnableGuardModeWhen(IBDAIControl pilot, Func condition) + { + yield return new WaitUntilFixed(condition); + if (pilot == null || pilot.vessel == null) yield break; + if (pilot.weaponManager != null && !pilot.weaponManager.guardMode) + { + competitionStatus.Add($"Enabling guard mode for {pilot.vessel.vesselName}"); + pilot.weaponManager.ToggleGuardMode(); + } + } + + void AdjustKerbalDrag(float speedThreshold, float scale) + { + foreach (var weaponManager in LoadedVesselSwitcher.Instance.WeaponManagers.SelectMany(tm => tm.Value)) + { + if (weaponManager.vessel.speed > speedThreshold) + { + if (dragLimiting.Contains(weaponManager.vessel)) continue; // Already limiting. + StartCoroutine(DragLimit(weaponManager.vessel, speedThreshold, scale)); + } + } + } + HashSet dragLimiting = new HashSet(); + IEnumerator DragLimit(Vessel vessel, float speedThreshold, float scale) + { + if (dragLimiting.Contains(vessel)) yield break; // Already limiting. + dragLimiting.Add(vessel); + var kerbals = VesselModuleRegistry.GetKerbalEVAs(vessel).Where(kerbal => kerbal != null).ToList(); + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log($"[BDArmory.BDACompetitionMode]: {vessel.vesselName} is over the speed limit, applying drag to {string.Join(", ", kerbals.Select(kerbal => kerbal.name))}"); + var wait = new WaitForFixedUpdate(); + foreach (var kerbal in kerbals) kerbal.part.ShieldedFromAirstream = false; + while (vessel != null && vessel.speed > speedThreshold) + { + var drag = (float)(vessel.speed - speedThreshold) * scale; + bool hasKerbal = false; + foreach (var kerbal in kerbals) + { + if (kerbal == null || kerbal.vessel != vessel) continue; + hasKerbal = true; + kerbal.part.minimum_drag = drag; + kerbal.part.maximum_drag = drag; + } + if (!hasKerbal) + { + var AI = VesselModuleRegistry.GetModule(vessel); + if (AI != null && AI.pilotEnabled) AI.DeactivatePilot(); + StartCoroutine(DelayedExplodeWMs(vessel, 1f, UncontrolledReason.Uncontrolled)); + } + yield return wait; + } + if (vessel != null) + { + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log($"[BDArmory.BDACompetitionMode]: {vessel.vesselName} is back within the speed limit, removing drag from {string.Join(", ", kerbals.Where(kerbal => kerbal != null).Select(kerbal => kerbal.name))}"); + foreach (var kerbal in kerbals) + { + if (kerbal == null) continue; + kerbal.part.ShieldedFromAirstream = true; + } + dragLimiting.Remove(vessel); + } + } #endregion #region Debris clean-up @@ -2646,7 +2216,7 @@ public void RemoveNonCompetitors(bool now = false) if (VesselModuleRegistry.ignoredVesselTypes.Contains(vessel.vesselType)) continue; // Debris handled by DebrisDelayedCleanUp, others are ignored. if (nonCompetitorsToRemove.Contains(vessel)) continue; // Already scheduled for removal. bool activePilot = false; - if (BDArmorySettings.RUNWAY_PROJECT && vessel.GetName() == "Pinata") + if (BDArmorySettings.RUNWAY_PROJECT && vessel.GetName() == BDArmorySettings.PINATA_NAME) { activePilot = true; } @@ -2716,6 +2286,30 @@ void DebrisDelayedCleanUp(Vessel debris) } } + void CometCleanup(bool disableSpawning = false) + { + if ((Versioning.version_major == 1 && Versioning.version_minor > 9) || Versioning.version_major > 1) // Introduced in 1.10 + { + CometCleanup_1_10(disableSpawning); + } + else // Nothing, comets didn't exist before + { + } + } + + void CometCleanup_1_10(bool disableSpawning = false) // KSP has issues on older versions if this call is in the parent function. + { + if (disableSpawning) + { + DisableCometSpawning(); + GameEvents.onCometSpawned.Add(RemoveCometVessel); + } + else + { + GameEvents.onCometSpawned.Remove(RemoveCometVessel); + } + } + void RemoveCometVessel(Vessel vessel) { if (vessel.vesselType == VesselType.SpaceObject) @@ -2731,12 +2325,14 @@ private IEnumerator DelayedVesselRemovalCoroutine(Vessel vessel, float delay) yield return new WaitForSeconds(delay); if (vessel != null && debrisTypes.Contains(vesselType) && !debrisTypes.Contains(vessel.vesselType)) { - Debug.Log("[BDArmory.BDACompetitionMode]: Debris " + vessel.vesselName + " is no longer labelled as debris, not removing."); + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode]: Debris " + vessel.vesselName + " is no longer labelled as debris, not removing."); yield break; } if (vessel != null) { - if (vessel.parts.Count == 1 && vessel.parts[0].IsKerbalEVA()) // The vessel is a kerbal on EVA. Ignore it for now. + if (VesselSpawnerWindow.Instance.Observers.Contains(vessel) // Ignore observers. + || (vessel.parts.Count == 1 && vessel.parts[0].IsKerbalEVA()) // The vessel is a kerbal on EVA. Ignore it for now. + ) { // KerbalSafetyManager.Instance.CheckForFallingKerbals(vessel); if (nonCompetitorsToRemove.Contains(vessel)) nonCompetitorsToRemove.Remove(vessel); @@ -2747,14 +2343,13 @@ private IEnumerator DelayedVesselRemovalCoroutine(Vessel vessel, float delay) } else { - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log("[BDArmory.BDACompetitionMode]: Removing " + vessel.vesselName); + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode]: Removing " + vessel.vesselName); yield return SpawnUtilsInstance.Instance.RemoveVesselCoroutine(vessel); } } if (nonCompetitorsToRemove.Contains(vessel)) { - if (vessel.vesselName != null) { Debug.Log($"[BDArmory.BDACompetitionMode]: {vessel.vesselName} removed."); } + if (BDArmorySettings.DEBUG_COMPETITION && vessel.vesselName != null) { Debug.Log($"[BDArmory.BDACompetitionMode]: {vessel.vesselName} removed."); } nonCompetitorsToRemove.Remove(vessel); } } @@ -2793,11 +2388,13 @@ void FixedUpdate() public void DoUpdate() { if (competitionStartTime < 0) return; // Note: this is the same condition as competitionIsActive and could probably be dropped. - if (competitionType == CompetitionType.WAYPOINTS) return; // Don't do anything below when running waypoints (for now). + if (competitionType == CompetitionType.WAYPOINTS && BDArmorySettings.RUNWAY_PROJECT_ROUND != 55) return; // Don't do anything below when running waypoints (for now). + if (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 55 && competitionIsActive && competitionIsActive) AdjustKerbalDrag(605, 0.01f); // Over 605m/s, add drag at a rate of 0.01 per m/s. + // Example usage of UpcomingCollisions(). Note that the timeToCPA values are only updated after an interval of half the current timeToCPA. // if (competitionIsActive) // foreach (var upcomingCollision in UpcomingCollisions(100f).Take(3)) - // Debug.Log("DEBUG Upcoming potential collision between " + upcomingCollision.Key.Item1 + " and " + upcomingCollision.Key.Item2 + " at distance " + Mathf.Sqrt(upcomingCollision.Value.Item1) + "m in " + upcomingCollision.Value.Item2 + "s."); + // Debug.Log("[BDArmory.BDACompetitionMode]: Upcoming potential collision between " + upcomingCollision.Key.Item1 + " and " + upcomingCollision.Key.Item2 + " at distance " + BDAMath.Sqrt(upcomingCollision.Value.Item1) + "m in " + upcomingCollision.Value.Item2 + "s."); var now = Planetarium.GetUniversalTime(); if (now < nextUpdateTick) return; @@ -2974,7 +2571,7 @@ public void DoUpdate() { var pilotAI = VesselModuleRegistry.GetModule(vessel); // Get the pilot AI if the vessel has one. var surfaceAI = VesselModuleRegistry.GetModule(vessel); // Get the surface AI if the vessel has one. - var vtolAI = VesselModuleRegistry.GetModule(vessel); // Get the VTOL AI if the vessel has one. + var vtolAI = VesselModuleRegistry.GetModule(vessel); // Get the VTOL AI if the vessel has one. if ((pilotAI == null && surfaceAI == null && vtolAI == null) || (mf.outOfAmmo && (BDArmorySettings.DISABLE_RAMMING || !(pilotAI != null && pilotAI.allowRamming)))) // if we've lost the AI or the vessel is out of weapons/ammo and ramming is not allowed. mf.guardMode = false; } @@ -2988,11 +2585,31 @@ public void DoUpdate() vData.averageCount++; if (vData.landedState && BDArmorySettings.COMPETITION_KILL_TIMER > 0) { - KillTimer[vesselName] = (int)(now - vData.landedKillTimer); - if (now - vData.landedKillTimer > BDArmorySettings.COMPETITION_KILL_TIMER) + if (VesselModuleRegistry.GetBDModuleSurfaceAI(vessel, true) == null) // Ignore surface AI vessels for the kill timer. + { + KillTimer[vesselName] = (int)(now - vData.landedKillTimer); + if (now - vData.landedKillTimer > BDArmorySettings.COMPETITION_KILL_TIMER) + { + vesselsToKill.Add(mf.vessel); + } + } + } + if (vData.AltitudeKillTimer > 0 && BDArmorySettings.COMPETITION_KILL_TIMER > 0) + { + KillTimer[vesselName] = (int)(now - vData.AltitudeKillTimer); + if (now - vData.AltitudeKillTimer > BDArmorySettings.COMPETITION_KILL_TIMER) { - var srfVee = VesselModuleRegistry.GetBDModuleSurfaceAI(vessel, true); - if (srfVee == null) vesselsToKill.Add(mf.vessel); //don't kill timer remove surface Ai vessels + var killerName = Scores.ScoreData[vesselName].lastPersonWhoDamagedMe; + if (killerName == "") + { + killerName = "Restricted Altitude!"; + Scores.ScoreData[vesselName].lastPersonWhoDamagedMe = killerName; + } + Scores.RegisterDeath(vesselName, GMKillReason.GM); + competitionStatus.Add(vesselName + " no fly zone!"); + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: " + vesselName + ":REMOVED:" + killerName); + if (KillTimer.ContainsKey(vesselName)) KillTimer.Remove(vesselName); + VesselUtils.ForceDeadVessel(vessel); } } else if (KillTimer.ContainsKey(vesselName)) @@ -3002,23 +2619,26 @@ public void DoUpdate() } string aliveString = string.Join(",", alive.ToArray()); previousNumberCompetitive = numberOfCompetitiveVessels; - // if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "] STILLALIVE: " + aliveString); // This just fills the logs needlessly. - if (BDArmorySettings.RUNWAY_PROJECT) + // if (BDArmorySettings.DEBUG_LABELS) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "] STILLALIVE: " + aliveString); // This just fills the logs needlessly. + if (BDArmorySettings.RUNWAY_PROJECT && hasPinata && !String.IsNullOrEmpty(BDArmorySettings.PINATA_NAME)) { // If we find a vessel named "Pinata" that's a special case object // this should probably be configurable. - if (!pinataAlive && alive.Contains("Pinata")) + if (!pinataAlive && alive.Contains(BDArmorySettings.PINATA_NAME)) { Debug.Log("[BDArmory.BDACompetitionMode" + CompetitionID.ToString() + "]: Setting Pinata Flag to Alive!"); pinataAlive = true; competitionStatus.Add("Enabling Pinata"); } - else if (pinataAlive && !alive.Contains("Pinata")) + else if (pinataAlive && !alive.Contains(BDArmorySettings.PINATA_NAME)) { // switch everyone onto separate teams when the Pinata Dies LoadedVesselSwitcher.Instance.MassTeamSwitch(true); pinataAlive = false; - competitionStatus.Add("Pinata is dead - competition is now a Free for all"); + competitionStatus.Add("Pinata killed by " + Scores.ScoreData[BDArmorySettings.PINATA_NAME].lastPersonWhoDamagedMe + "! Competition is now a Free for all"); + Scores.RegisterMissileStrike(Scores.ScoreData[BDArmorySettings.PINATA_NAME].lastPersonWhoDamagedMe, BDArmorySettings.PINATA_NAME); //give a missile strike point to indicate the pinata kill on the web API + if (BDArmorySettings.AUTO_ENABLE_VESSEL_SWITCHING) + LoadedVesselSwitcher.Instance.EnableAutoVesselSwitching(true); // start kill clock if (!killerGMenabled) { @@ -3034,7 +2654,7 @@ public void DoUpdate() // check everyone who's no longer alive if (!alive.Contains(player)) { - if (BDArmorySettings.RUNWAY_PROJECT && player == "Pinata") continue; + if (BDArmorySettings.RUNWAY_PROJECT && player == BDArmorySettings.PINATA_NAME) continue; if (Scores.ScoreData[player].aliveState == AliveState.Alive) { var timeOfDeath = now; @@ -3042,9 +2662,10 @@ public void DoUpdate() if (rammingInformation.ContainsKey(player) && rammingInformation[player].targetInformation.Values.Any(other => other.collisionDetected)) { rammingInformation[player].timeOfDeath = rammingInformation[player].targetInformation.Values.Where(other => other.collisionDetected).Select(other => other.collisionDetectedTime).Max(); - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log($"[BDArmory.BDACompetitionMode:{CompetitionID}]: Delaying death of {player} due to being involved in a collision {now - rammingInformation[player].timeOfDeath}s ago at {rammingInformation[player].timeOfDeath - competitionStartTime:F3}."); + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log($"[BDArmory.BDACompetitionMode:{CompetitionID}]: Delaying death of {player} due to being involved in a collision {now - rammingInformation[player].timeOfDeath}s ago at {rammingInformation[player].timeOfDeath - competitionStartTime:F3}."); continue; // Involved in a collision, delay registering death. } + if (asteroidCollisions.Contains(player)) continue; // Also delay registering death if they're colliding with an asteroid. switch (Scores.ScoreData[player].lastDamageWasFrom) { case DamageFrom.Ramming: @@ -3072,6 +2693,9 @@ public void DoUpdate() case DamageFrom.Ramming: statusMessage += " was rammed by "; break; + case DamageFrom.Asteroids: + statusMessage += " flew into an asteroid "; + break; case DamageFrom.Incompetence: statusMessage += " CRASHED and BURNED."; break; @@ -3123,13 +2747,13 @@ public void DoUpdate() break; } competitionStatus.Add(statusMessage); - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log($"[BDArmory.BDACompetitionMode:{CompetitionID}]: " + statusMessage); + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log($"[BDArmory.BDACompetitionMode:{CompetitionID}]: " + statusMessage); if (BDArmorySettings.MUTATOR_MODE && BDArmorySettings.MUTATOR_APPLY_KILL) { if (BDArmorySettings.MUTATOR_LIST.Count > 0 && canAssignMutator) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log($"[BDArmory.BDACompetitionMode:{CompetitionID}]: Assigning On Kill mutator to " + Scores.ScoreData[player].lastPersonWhoDamagedMe); + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log($"[BDArmory.BDACompetitionMode:{CompetitionID}]: Assigning On Kill mutator to " + Scores.ScoreData[player].lastPersonWhoDamagedMe); using (var loadedVessels = BDATargetManager.LoadedVessels.GetEnumerator()) while (loadedVessels.MoveNext()) @@ -3162,7 +2786,7 @@ public void DoUpdate() deadOrAlive = deadOrAliveString; var numberOfCompetitiveTeams = LoadedVesselSwitcher.Instance.WeaponManagers.Count; - if (now - competitionStartTime > BDArmorySettings.COMPETITION_INITIAL_GRACE_PERIOD && (numberOfCompetitiveVessels < 2 || (!BDArmorySettings.TAG_MODE && numberOfCompetitiveTeams < 2)) && !ContinuousSpawning.Instance.vesselsSpawningContinuously) + if (now - competitionStartTime > BDArmorySettings.COMPETITION_INITIAL_GRACE_PERIOD && (numberOfCompetitiveVessels < competitiveTeamsAliveLimit || (!BDArmorySettings.TAG_MODE && numberOfCompetitiveTeams < competitiveTeamsAliveLimit)) && !ContinuousSpawning.Instance.vesselsSpawningContinuously) { if (finalGracePeriodStart < 0) finalGracePeriodStart = now; @@ -3173,8 +2797,9 @@ public void DoUpdate() { competitionStatus.Add(key + " wins the round!"); } - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]:No viable competitors, Automatically dumping scores"); + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]:No viable competitors, Automatically dumping scores"); StopCompetition(); + return; } } @@ -3184,7 +2809,7 @@ public void DoUpdate() int maxVesselsActive = (ContinuousSpawning.Instance.vesselsSpawningContinuously && BDArmorySettings.VESSEL_SPAWN_CONCURRENT_VESSELS > 0) ? BDArmorySettings.VESSEL_SPAWN_CONCURRENT_VESSELS : Scores.Players.Count; double time = now - competitionStartTime; gravityMultiplier = 1f + 7f * (float)(Scores.deathCount % maxVesselsActive) / (float)(maxVesselsActive - 1); // From 1G to 8G. - gravityMultiplier += ContinuousSpawning.Instance.vesselsSpawningContinuously ? Mathf.Sqrt(5f - 5f * Mathf.Cos((float)time / 600f * Mathf.PI)) : Mathf.Sqrt((float)time / 60f); // Plus up to 3.16G. + gravityMultiplier += ContinuousSpawning.Instance.vesselsSpawningContinuously ? BDAMath.Sqrt(5f - 5f * Mathf.Cos((float)time / 600f * Mathf.PI)) : BDAMath.Sqrt((float)time / 60f); // Plus up to 3.16G. PhysicsGlobals.GraviticForceMultiplier = (double)gravityMultiplier; VehiclePhysics.Gravity.Refresh(); if (Mathf.RoundToInt(10 * gravityMultiplier) != Mathf.RoundToInt(10 * lastGravityMultiplier)) // Only write a message when it shows something different. @@ -3194,6 +2819,29 @@ public void DoUpdate() } } + //Set MinAlt + if (BDArmorySettings.ALTITUDE_HACKS && competitionIsActive) + { + int maxVesselsActive = (ContinuousSpawning.Instance.vesselsSpawningContinuously && BDArmorySettings.VESSEL_SPAWN_CONCURRENT_VESSELS > 0) ? BDArmorySettings.VESSEL_SPAWN_CONCURRENT_VESSELS : Scores.Players.Count; + double time = now - competitionStartTime; + MinAlt = 20 + 8000f * (float)(Scores.deathCount % maxVesselsActive) / (float)(maxVesselsActive - 1); // From 1km to 8km. + MinAlt += (ContinuousSpawning.Instance.vesselsSpawningContinuously ? BDAMath.Sqrt(5f - 5f * Mathf.Cos((float)time / 600f * Mathf.PI)) : BDAMath.Sqrt((float)time / 60f)) * 1000; // Plus up to 3.16km. + + using (List.Enumerator pilots = GetAllPilots().GetEnumerator()) + { + while (pilots.MoveNext()) + { + var pilotAI = VesselModuleRegistry.GetModule(pilots.Current.vessel); // Get the pilot AI if the vessel has one. + pilotAI.minAltitude = MinAlt; + } + } + if (Mathf.RoundToInt(MinAlt / 100) != Mathf.RoundToInt(lastMinAlt / 100)) // Only write a message when it shows something different. + { + lastMinAlt = MinAlt; + competitionStatus.Add("Competition: Adjusting min Altitude to " + MinAlt.ToString("0.0") + "m!"); + } + } + // use the exploder system to remove vessels that should be nuked foreach (var vessel in vesselsToKill) { @@ -3210,15 +2858,17 @@ public void DoUpdate() Scores.RegisterDeath(vesselName, GMKillReason.LandedTooLong); competitionStatus.Add(vesselName + " was landed too long."); } - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: " + vesselName + ":REMOVED:" + killerName); + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: " + vesselName + ":REMOVED:" + killerName); if (KillTimer.ContainsKey(vesselName)) KillTimer.Remove(vesselName); - Utils.ForceDeadVessel(vessel); + VesselUtils.ForceDeadVessel(vessel); } if (!(BDArmorySettings.COMPETITION_NONCOMPETITOR_REMOVAL_DELAY > 60)) RemoveNonCompetitors(); - CheckAltitudeLimits(); + if (now - competitionStartTime > altitudeLimitGracePeriod) + CheckAltitudeLimits(); + if (competitionIsActive && competitionType == CompetitionType.WAYPOINTS && BDArmorySettings.COMPETITION_WAYPOINTS_GM_KILL_PERIOD > 0 && now - competitionStartTime > BDArmorySettings.COMPETITION_INITIAL_GRACE_PERIOD) CullSlowWaypointRunners(BDArmorySettings.COMPETITION_WAYPOINTS_GM_KILL_PERIOD); if (BDArmorySettings.RUNWAY_PROJECT) { FindVictim(); @@ -3227,14 +2877,18 @@ public void DoUpdate() if (BDArmorySettings.COMPETITION_DURATION > 0 && now - competitionStartTime >= BDArmorySettings.COMPETITION_DURATION * 60d) { - LogResults("due to out-of-time"); + var message = "Ending competition due to out-of-time."; + competitionStatus.Add(message); + Debug.Log($"[BDArmory.BDACompetitionMode:{CompetitionID.ToString()}]: " + message); + LogResults(message: "due to out-of-time", tag: competitionTag); StopCompetition(); + return; } if ((BDArmorySettings.MUTATOR_MODE && BDArmorySettings.MUTATOR_APPLY_TIMER) && BDArmorySettings.MUTATOR_DURATION > 0 && now - MutatorResetTime >= BDArmorySettings.MUTATOR_DURATION * 60d && BDArmorySettings.MUTATOR_LIST.Count > 0) { - ScreenMessages.PostScreenMessage(Localizer.Format("#LOC_BDArmory_UI_MutatorShuffle"), 5, ScreenMessageStyle.UPPER_CENTER); + ScreenMessages.PostScreenMessage(StringUtils.Localize("#LOC_BDArmory_UI_MutatorShuffle"), 5, ScreenMessageStyle.UPPER_CENTER); ConfigureMutator(); foreach (var vessel in FlightGlobals.Vessels) { @@ -3268,7 +2922,7 @@ public void LogResults(string message = "", string tag = "") { if (competitionStartTime < 0) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BDACompetitionMode]: No active competition, not dumping results."); + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode]: No active competition, not dumping results."); return; } // RunDebugChecks(); @@ -3406,7 +3060,7 @@ public void UpdateTimesToCPAs() { if (currentTime - rammingInformation[vesselName].targetInformation[otherVesselName].lastUpdateTime > rammingInformation[vesselName].targetInformation[otherVesselName].timeToCPA / 2f) // When half the time is gone, update it. { - float timeToCPA = AIUtils.ClosestTimeToCPA(vessel, otherVessel, maxTimeToCPA); // Look up to maxTimeToCPA ahead. + float timeToCPA = AIUtils.TimeToCPA(vessel, otherVessel, maxTimeToCPA); // Look up to maxTimeToCPA ahead. if (timeToCPA > 0f && timeToCPA < maxTimeToCPA) // If the closest approach is within the next maxTimeToCPA, log it. rammingInformation[vesselName].targetInformation[otherVesselName].timeToCPA = timeToCPA; else // Otherwise set it to the max value. @@ -3454,17 +3108,17 @@ private void CheckForPotentialCollisions() { if (!rammingInformation.ContainsKey(otherVesselName)) { - Debug.Log("DEBUG other vessel (" + otherVesselName + ") is missing from rammingInformation!"); + Debug.Log("[BDArmory.BDACompetitionMode]: other vessel (" + otherVesselName + ") is missing from rammingInformation!"); return; } if (!rammingInformation[vesselName].targetInformation.ContainsKey(otherVesselName)) { - Debug.Log("DEBUG other vessel (" + otherVesselName + ") is missing from rammingInformation[vessel].targetInformation!"); + Debug.Log("[BDArmory.BDACompetitionMode]: other vessel (" + otherVesselName + ") is missing from rammingInformation[vessel].targetInformation!"); return; } if (!rammingInformation[otherVesselName].targetInformation.ContainsKey(vesselName)) { - Debug.Log("DEBUG vessel (" + vesselName + ") is missing from rammingInformation[otherVessel].targetInformation!"); + Debug.Log("[BDArmory.BDACompetitionMode]: vessel (" + vesselName + ") is missing from rammingInformation[otherVessel].targetInformation!"); return; } var otherVessel = rammingInformation[vesselName].targetInformation[otherVesselName].vessel; @@ -3493,7 +3147,7 @@ private void CheckForPotentialCollisions() { if (rammingInformation[vesselName].partCount != vessel.parts.Count) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BDACompetitionMode]: Ram logging: " + vesselName + " lost " + (rammingInformation[vesselName].partCount - vessel.parts.Count) + " parts from getting shot."); + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode]: Ram logging: " + vesselName + " lost " + (rammingInformation[vesselName].partCount - vessel.parts.Count) + " parts from getting shot."); rammingInformation[vesselName].partCount = vessel.parts.Count; } } @@ -3502,7 +3156,7 @@ private void CheckForPotentialCollisions() { if (rammingInformation[otherVesselName].partCount != otherVessel.parts.Count) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BDACompetitionMode]: Ram logging: " + otherVesselName + " lost " + (rammingInformation[otherVesselName].partCount - otherVessel.parts.Count) + " parts from getting shot."); + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode]: Ram logging: " + otherVesselName + " lost " + (rammingInformation[otherVesselName].partCount - otherVessel.parts.Count) + " parts from getting shot."); rammingInformation[otherVesselName].partCount = otherVessel.parts.Count; } } @@ -3548,59 +3202,75 @@ private void AnalyseCollision(EventReport data) var vessel = data.origin.vessel; if (vessel == null) // Can vessel be null here? It doesn't appear so. { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BDACompetitionMode]: Ram logging: in AnalyseCollision the colliding part belonged to a null vessel!"); + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode]: Ram logging: in AnalyseCollision the colliding part belonged to a null vessel!"); return; } try { + // Debug.Log($"DEBUG Collision of {vessel.vesselName} with: other:{data.other}, sender: {data.sender}, stage: {data.stage}, msg: {data.msg}, param: {data.param}, type: {data.eventType}"); bool hitVessel = false; if (rammingInformation.ContainsKey(vessel.vesselName)) // If the part was attached to a vessel, { var vesselName = vessel.vesselName; // For convenience. - var destroyedPotentialColliders = new List(); - foreach (var otherVesselName in rammingInformation[vesselName].targetInformation.Keys) // for each other vessel, - if (rammingInformation[vesselName].targetInformation[otherVesselName].potentialCollision) // if it was potentially about to collide, - { - var otherVessel = rammingInformation[vesselName].targetInformation[otherVesselName].vessel; - if (otherVessel == null) // Vessel that was potentially colliding has been destroyed. It's more likely that an alive potential collider is the real collider, so remember it in case there are no living potential colliders. - { - destroyedPotentialColliders.Add(otherVesselName); - continue; - } - var separation = Vector3.Magnitude(vessel.transform.position - otherVessel.transform.position); - if (separation < collisionMargin * (rammingInformation[vesselName].radius + rammingInformation[otherVesselName].radius)) // and their separation is less than the sum of their radii, + if (data.other.StartsWith("Ast. ")) // We hit an asteroid, most likely due to one of the asteroids game modes. + { + if (!asteroidCollisions.Contains(vesselName)) + StartCoroutine(AsteroidCollision(vessel, rammingInformation[vesselName].partCount)); + } + else + { + var destroyedPotentialColliders = new List(); + foreach (var otherVesselName in rammingInformation[vesselName].targetInformation.Keys) // for each other vessel, + if (rammingInformation[vesselName].targetInformation[otherVesselName].potentialCollision) // if it was potentially about to collide, { - if (!rammingInformation[vesselName].targetInformation[otherVesselName].collisionDetected) // Take the values when the collision is first detected. + var otherVessel = rammingInformation[vesselName].targetInformation[otherVesselName].vessel; + if (otherVessel == null) // Vessel that was potentially colliding has been destroyed. It's more likely that an alive potential collider is the real collider, so remember it in case there are no living potential colliders. { - rammingInformation[vesselName].targetInformation[otherVesselName].collisionDetected = true; // register it as involved in the collision. We'll check for damaged parts in CheckForDamagedParts. - rammingInformation[otherVesselName].targetInformation[vesselName].collisionDetected = true; // The information is symmetric. - rammingInformation[vesselName].targetInformation[otherVesselName].partCountJustPriorToCollision = rammingInformation[otherVesselName].partCount; - rammingInformation[otherVesselName].targetInformation[vesselName].partCountJustPriorToCollision = rammingInformation[vesselName].partCount; - rammingInformation[vesselName].targetInformation[otherVesselName].sqrDistance = (otherVessel != null) ? Vector3.SqrMagnitude(vessel.CoM - otherVessel.CoM) : (Mathf.Pow(collisionMargin * (rammingInformation[vesselName].radius + rammingInformation[otherVesselName].radius), 2f) + 1f); // FIXME Should destroyed vessels have 0 sqrDistance instead? - rammingInformation[otherVesselName].targetInformation[vesselName].sqrDistance = rammingInformation[vesselName].targetInformation[otherVesselName].sqrDistance; - rammingInformation[vesselName].targetInformation[otherVesselName].collisionDetectedTime = currentTime; - rammingInformation[otherVesselName].targetInformation[vesselName].collisionDetectedTime = currentTime; - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BDACompetitionMode]: Ram logging: Collision detected between " + vesselName + " and " + otherVesselName); + destroyedPotentialColliders.Add(otherVesselName); + continue; + } + var separation = Vector3.Magnitude(vessel.transform.position - otherVessel.transform.position); + if (separation < collisionMargin * (rammingInformation[vesselName].radius + rammingInformation[otherVesselName].radius)) // and their separation is less than the sum of their radii, + { + if (!rammingInformation[vesselName].targetInformation[otherVesselName].collisionDetected) // Take the values when the collision is first detected. + { + rammingInformation[vesselName].targetInformation[otherVesselName].collisionDetected = true; // register it as involved in the collision. We'll check for damaged parts in CheckForDamagedParts. + rammingInformation[otherVesselName].targetInformation[vesselName].collisionDetected = true; // The information is symmetric. + rammingInformation[vesselName].targetInformation[otherVesselName].partCountJustPriorToCollision = rammingInformation[otherVesselName].partCount; + rammingInformation[otherVesselName].targetInformation[vesselName].partCountJustPriorToCollision = rammingInformation[vesselName].partCount; + if (otherVessel is not null) + rammingInformation[vesselName].targetInformation[otherVesselName].sqrDistance = (vessel.CoM - otherVessel.CoM).sqrMagnitude; + else + { + var distance = collisionMargin * (rammingInformation[vesselName].radius + rammingInformation[otherVesselName].radius); + rammingInformation[vesselName].targetInformation[otherVesselName].sqrDistance = distance * distance + 1f; + } + rammingInformation[otherVesselName].targetInformation[vesselName].sqrDistance = rammingInformation[vesselName].targetInformation[otherVesselName].sqrDistance; + rammingInformation[vesselName].targetInformation[otherVesselName].collisionDetectedTime = currentTime; + rammingInformation[otherVesselName].targetInformation[vesselName].collisionDetectedTime = currentTime; + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode]: Ram logging: Collision detected between " + vesselName + " and " + otherVesselName); + } + hitVessel = true; } - hitVessel = true; } - } - if (!hitVessel) // No other living vessels were potential targets, add in the destroyed ones (if any). - { - foreach (var otherVesselName in destroyedPotentialColliders) // Note: if there are more than 1, then multiple craft could be credited with the kill, but this is unlikely. + if (!hitVessel) // No other living vessels were potential targets, add in the destroyed ones (if any). { - rammingInformation[vesselName].targetInformation[otherVesselName].collisionDetected = true; // register it as involved in the collision. We'll check for damaged parts in CheckForDamagedParts. - rammingInformation[otherVesselName].targetInformation[vesselName].collisionDetected = true; // The information is symmetric. - rammingInformation[vesselName].targetInformation[otherVesselName].partCountJustPriorToCollision = rammingInformation[otherVesselName].partCount; - rammingInformation[otherVesselName].targetInformation[vesselName].partCountJustPriorToCollision = rammingInformation[vesselName].partCount; - rammingInformation[vesselName].targetInformation[otherVesselName].collisionDetectedTime = currentTime; - rammingInformation[otherVesselName].targetInformation[vesselName].collisionDetectedTime = currentTime; - hitVessel = true; + foreach (var otherVesselName in destroyedPotentialColliders) // Note: if there are more than 1, then multiple craft could be credited with the kill, but this is unlikely. + { + rammingInformation[vesselName].targetInformation[otherVesselName].collisionDetected = true; // register it as involved in the collision. We'll check for damaged parts in CheckForDamagedParts. + rammingInformation[otherVesselName].targetInformation[vesselName].collisionDetected = true; // The information is symmetric. + rammingInformation[vesselName].targetInformation[otherVesselName].partCountJustPriorToCollision = rammingInformation[otherVesselName].partCount; + rammingInformation[otherVesselName].targetInformation[vesselName].partCountJustPriorToCollision = rammingInformation[vesselName].partCount; + rammingInformation[vesselName].targetInformation[otherVesselName].collisionDetectedTime = currentTime; + rammingInformation[otherVesselName].targetInformation[vesselName].collisionDetectedTime = currentTime; + hitVessel = true; + } } } if (!hitVessel) // We didn't hit another vessel, maybe it crashed and died. { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BDACompetitionMode]: Ram logging: " + vesselName + " hit something else."); + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log($"[BDArmory.BDACompetitionMode]: Ram logging: {vesselName} hit {data.other}."); + rammingInformation[vesselName].partCount = vessel.parts.Count; // Update the vessel part count. foreach (var otherVesselName in rammingInformation[vesselName].targetInformation.Keys) { rammingInformation[vesselName].targetInformation[otherVesselName].potentialCollision = false; // Set potential collisions to false. @@ -3630,6 +3300,40 @@ private void AnalyseCollision(EventReport data) } } + HashSet asteroidCollisions = new HashSet(); + IEnumerator AsteroidCollision(Vessel vessel, int preCollisionPartCount) + { + if (vessel == null) yield break; + var vesselName = vessel.vesselName; + var partsLost = preCollisionPartCount; + var timeOfDeath = Planetarium.GetUniversalTime(); // In case they die. + asteroidCollisions.Add(vesselName); + yield return new WaitForSecondsFixed(potentialCollisionDetectionTime); + if (vessel == null || VesselModuleRegistry.GetMissileFire(vessel) == null) + { + rammingInformation[vesselName].partCount = 0; + if (Scores.ScoreData[vesselName].aliveState == AliveState.Alive) + { + Scores.RegisterAsteroidCollision(vesselName, partsLost); + Scores.RegisterDeath(vesselName, GMKillReason.Asteroids, timeOfDeath); + competitionStatus.Add($"{vesselName} flew into an asteroid and died!"); + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log($"[BDArmory.BDACompetitionMode]: {vesselName} flew into an asteroid and died!"); + } + } + else + { + partsLost -= vessel.parts.Count; + rammingInformation[vesselName].partCount = vessel.parts.Count; + if (Scores.ScoreData[vesselName].aliveState == AliveState.Alive) + { + Scores.RegisterAsteroidCollision(vesselName, partsLost); + competitionStatus.Add($"{vesselName} flew into an asteroid and lost {partsLost} parts!"); + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log($"[BDArmory.BDACompetitionMode]: {vesselName} flew into an asteroid and lost {partsLost} parts!"); + } + } + asteroidCollisions.Remove(vesselName); + } + // Check for parts being lost on the various vessels for which collisions have been detected. private void CheckForDamagedParts() { @@ -3686,7 +3390,7 @@ private void CheckForDamagedParts() if (collidingVesselDistances.Count > 0) collidingVesselsBySeparation.Add(vesselName, new KeyValuePair>>(collidingVessels.First().Value, collidingVessels)); - if (BDArmorySettings.DRAW_DEBUG_LABELS && collidingVesselDistances.Count > 1) // DEBUG + if (BDArmorySettings.DEBUG_COMPETITION && collidingVesselDistances.Count > 1) // DEBUG { foreach (var otherVesselName in collidingVesselDistances.Keys) Debug.Log("[BDArmory.BDACompetitionMode]: Ram logging: colliding vessel distances^2 from " + vesselName + ": " + otherVesselName + " " + collidingVesselDistances[otherVesselName]); foreach (var otherVesselName in collidingVessels) Debug.Log("[BDArmory.BDACompetitionMode]: Ram logging: sorted order: " + otherVesselName.Key); @@ -3712,6 +3416,20 @@ private void CheckForDamagedParts() // Count the number of parts lost. var rammedPartsLost = (otherPilotAI == null) ? rammingInformation[vesselName].targetInformation[otherVesselName].partCountJustPriorToCollision : rammingInformation[vesselName].targetInformation[otherVesselName].partCountJustPriorToCollision - otherVessel.parts.Count; var rammingPartsLost = (pilotAI == null) ? rammingInformation[otherVesselName].targetInformation[vesselName].partCountJustPriorToCollision : rammingInformation[otherVesselName].targetInformation[vesselName].partCountJustPriorToCollision - vessel.parts.Count; + if (rammedPartsLost < 0 || rammingPartsLost < 0) // BUG! A plane involved in two collisions close together apparently can cause this? + { + Debug.LogWarning($"[BDArmory.BDACompetitionMode]: Negative parts lost in ram! Clamping to 0."); + if (rammedPartsLost < 0) + { + Debug.LogWarning($"[BDArmory.BDACompetitionMode]: {otherVesselName} had {rammingInformation[vesselName].targetInformation[otherVesselName].partCountJustPriorToCollision} parts and lost {rammedPartsLost} parts (current part count: {(otherPilotAI == null ? "none" : $"{otherVessel.parts.Count}")})"); + rammedPartsLost = 0; + } + if (rammingPartsLost < 0) + { + Debug.LogWarning($"[BDArmory.BDACompetitionMode]: {vesselName} had {rammingInformation[otherVesselName].targetInformation[vesselName].partCountJustPriorToCollision} parts and lost {rammingPartsLost} parts (current part count: {(pilotAI == null ? "none" : $"{vessel.parts.Count}")})"); + rammingPartsLost = 0; + } + } rammingInformation[otherVesselName].partCount -= rammedPartsLost; // Immediately adjust the parts count for more accurate tracking. rammingInformation[vesselName].partCount -= rammingPartsLost; // Update any other collisions that are waiting to count parts. @@ -3806,7 +3524,7 @@ void CheckForMissingParts() { if (rammingInformation[vesselName].vessel == null) continue; partsCheck.Add(vesselName, rammingInformation[vesselName].vessel.parts.Count); - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BDACompetitionMode]: Ram logging: " + vesselName + " started with " + partsCheck[vesselName] + " parts."); + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode]: Ram logging: " + vesselName + " started with " + partsCheck[vesselName] + " parts."); } } foreach (var vesselName in rammingInformation.Keys) @@ -3817,13 +3535,13 @@ void CheckForMissingParts() { if (partsCheck[vesselName] != vessel.parts.Count) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BDACompetitionMode]: Ram logging: Parts Check: " + vesselName + " has lost " + (partsCheck[vesselName] - vessel.parts.Count) + " parts." + (vessel.parts.Count > 0 ? "" : " and is no more.")); + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode]: Ram logging: Parts Check: " + vesselName + " has lost " + (partsCheck[vesselName] - vessel.parts.Count) + " parts." + (vessel.parts.Count > 0 ? "" : " and is no more.")); partsCheck[vesselName] = vessel.parts.Count; } } else if (partsCheck[vesselName] > 0) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BDACompetitionMode]: Ram logging: Parts Check: " + vesselName + " has been destroyed, losing " + partsCheck[vesselName] + " parts."); + if (BDArmorySettings.DEBUG_COMPETITION) Debug.Log("[BDArmory.BDACompetitionMode]: Ram logging: Parts Check: " + vesselName + " has been destroyed, losing " + partsCheck[vesselName] + " parts."); partsCheck[vesselName] = 0; } } @@ -3837,7 +3555,7 @@ private void LogRamming() UpdateTimesToCPAs(); CheckForPotentialCollisions(); CheckForDamagedParts(); - if (BDArmorySettings.DRAW_DEBUG_LABELS) CheckForMissingParts(); // DEBUG + if (BDArmorySettings.DEBUG_COMPETITION) CheckForMissingParts(); // DEBUG } #endregion @@ -3849,7 +3567,7 @@ public void TagResetTeams() var pilots = GetAllPilots(); foreach (var pilot in pilots) { - if (!Scores.Players.Contains(pilot.vessel.GetName())) { Debug.Log("DEBUG 2 Scores doesn't contain " + pilot.vessel.GetName()); continue; } + if (!Scores.Players.Contains(pilot.vessel.GetName())) { Debug.Log("[BDArmory.BDACompetitionMode]: Scores doesn't contain " + pilot.vessel.GetName()); continue; } pilot.weaponManager.SetTeam(BDTeam.Get(T.ToString())); Scores.ScoreData[pilot.vessel.GetName()].tagIsIt = false; pilot.vessel.ActionGroups.ToggleGroup(KM_dictAG[9]); // Trigger AG9 on becoming "NOT IT" @@ -3871,7 +3589,7 @@ public void CheckMemoryUsage() // DEBUG strings.Add("Mono used: " + UnityEngine.Profiling.Profiler.GetMonoUsedSizeLong() / 1024 / 1024 + "MB"); strings.Add("GfxDriver: " + UnityEngine.Profiling.Profiler.GetAllocatedMemoryForGraphicsDriver() / 1024 / 1024 + "MB"); strings.Add("plus unspecified runtime (native) memory."); - Debug.Log("[BDArmory.BDACompetitionMode]: DEBUG Memory Usage: " + string.Join(", ", strings)); + Debug.Log("[BDArmory.BDACompetitionMode]: Memory Usage: " + string.Join(", ", strings)); } public void CheckNumbersOfThings() // DEBUG @@ -3914,7 +3632,7 @@ public void RunDebugChecks() { CheckMemoryUsage(); #if DEBUG - CheckNumbersOfThings(); + if (BDArmorySettings.DEBUG_SETTINGS_TOGGLE) CheckNumbersOfThings(); #endif } @@ -4180,28 +3898,35 @@ public void CleanUpKSPsDeadReferences() var toRemove = new List(); foreach (var key in FlightGlobals.PersistentVesselIds.Keys) if (FlightGlobals.PersistentVesselIds[key] == null) toRemove.Add(key); - Debug.Log($"DEBUG Found {toRemove.Count} null persistent vessel references."); + if (BDArmorySettings.DEBUG_SETTINGS_TOGGLE) Debug.Log($"[BDArmory.BDACompetitionMode]: DEBUG Found {toRemove.Count} null persistent vessel references."); foreach (var key in toRemove) FlightGlobals.PersistentVesselIds.Remove(key); toRemove.Clear(); foreach (var key in FlightGlobals.PersistentLoadedPartIds.Keys) if (FlightGlobals.PersistentLoadedPartIds[key] == null) toRemove.Add(key); - Debug.Log($"DEBUG Found {toRemove.Count} null persistent loaded part references."); + if (BDArmorySettings.DEBUG_SETTINGS_TOGGLE) Debug.Log($"[BDArmory.BDACompetitionMode]: DEBUG Found {toRemove.Count} null persistent loaded part references."); foreach (var key in toRemove) FlightGlobals.PersistentLoadedPartIds.Remove(key); // Usually doesn't find any. toRemove.Clear(); foreach (var key in FlightGlobals.PersistentUnloadedPartIds.Keys) if (FlightGlobals.PersistentUnloadedPartIds[key] == null) toRemove.Add(key); - Debug.Log($"DEBUG Found {toRemove.Count} null persistent unloaded part references."); + if (BDArmorySettings.DEBUG_SETTINGS_TOGGLE) Debug.Log($"[BDArmory.BDACompetitionMode]: DEBUG Found {toRemove.Count} null persistent unloaded part references."); foreach (var key in toRemove) FlightGlobals.PersistentUnloadedPartIds.Remove(key); var protoVessels = HighLogic.CurrentGame.flightState.protoVessels.Where(pv => pv.vesselRef == null).ToList(); - if (protoVessels.Count > 0) { Debug.Log($"DEBUG Found {protoVessels.Count} inactive ProtoVessels in flightState."); } + if (BDArmorySettings.DEBUG_SETTINGS_TOGGLE) if (protoVessels.Count > 0) { Debug.Log($"[BDArmory.BDACompetitionMode]: DEBUG Found {protoVessels.Count} inactive ProtoVessels in flightState."); } foreach (var protoVessel in protoVessels) { if (protoVessel == null) continue; - ShipConstruction.RecoverVesselFromFlight(protoVessel, HighLogic.CurrentGame.flightState, true); + try + { + ShipConstruction.RecoverVesselFromFlight(protoVessel, HighLogic.CurrentGame.flightState, true); + } + catch (Exception e) + { + Debug.LogError($"[BDArmory.BDACompetitionMode]: Exception thrown while removing vessel: {e.Message}"); + } if (protoVessel == null) continue; if (protoVessel.protoPartSnapshots != null) { diff --git a/BDArmory/Competition/BDATournament.cs b/BDArmory/Competition/BDATournament.cs index 2dda38f00..22953a535 100644 --- a/BDArmory/Competition/BDATournament.cs +++ b/BDArmory/Competition/BDATournament.cs @@ -6,20 +6,21 @@ using UnityEngine; using BDArmory.Competition.OrchestrationStrategies; -using BDArmory.Competition.SpawnStrategies; -using BDArmory.Competition.VesselSpawning; -using BDArmory.Core; using BDArmory.Evolution; -using BDArmory.Misc; +using BDArmory.GameModes.Waypoints; +using BDArmory.Settings; using BDArmory.UI; +using BDArmory.Utils; +using BDArmory.VesselSpawning.SpawnStrategies; +using BDArmory.VesselSpawning; namespace BDArmory.Competition { // A serializable configuration for loading and saving the tournament state. [Serializable] - public class RoundConfig : SpawnConfig + public class RoundConfig : CircularSpawnConfig { - public RoundConfig(int round, int heat, bool completed, SpawnConfig config) : base(config) { this.round = round; this.heat = heat; this.completed = completed; SerializeTeams(); } + public RoundConfig(int round, int heat, bool completed, CircularSpawnConfig config) : base(config) { this.round = round; this.heat = heat; this.completed = completed; SerializeTeams(); } public int round; public int heat; public bool completed; @@ -82,15 +83,17 @@ public class TournamentState public string savegame; private List craftFiles; // For FFA style tournaments. private List> teamFiles; // For teams style tournaments. + private List> opponentTeamFiles; // For gauntlet style tournaments. public int vesselCount; public int teamCount; public int teamsPerHeat; public int vesselsPerTeam; public bool fullTeams; public TournamentType tournamentType = TournamentType.FFA; - [NonSerialized] public Dictionary> rounds; // > + [NonSerialized] public Dictionary> rounds; // > [NonSerialized] public Dictionary> completed = new Dictionary>(); [NonSerialized] private List> teamSpawnQueues = new List>(); + [NonSerialized] private List> opponentTeamSpawnQueues = new List>(); private string message; /* Generate rounds and heats by shuffling the crafts list and breaking it into groups. @@ -130,7 +133,7 @@ public bool Generate(string folder, int numberOfRounds, int vesselsPerHeat, int fullHeatCount = craftFiles.Count / vesselsPerHeat; break; } - rounds = new Dictionary>(); + rounds = new Dictionary>(); switch (tournamentStyle) { case 0: // RNG @@ -144,18 +147,17 @@ public bool Generate(string folder, int numberOfRounds, int vesselsPerHeat, int int vesselsThisHeat = vesselsPerHeat; int count = 0; List selectedFiles = craftFiles.Take(vesselsThisHeat).ToList(); - rounds.Add(rounds.Count, new Dictionary()); + rounds.Add(rounds.Count, new Dictionary()); int heatIndex = 0; while (selectedFiles.Count > 0) { - rounds[roundIndex].Add(rounds[roundIndex].Count, new SpawnConfig( + rounds[roundIndex].Add(rounds[roundIndex].Count, new CircularSpawnConfig( BDArmorySettings.VESSEL_SPAWN_WORLDINDEX, BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.x, BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.y, - BDArmorySettings.VESSEL_SPAWN_ALTITUDE, + BDArmorySettings.VESSEL_SPAWN_ALTITUDE_, BDArmorySettings.VESSEL_SPAWN_DISTANCE_TOGGLE ? BDArmorySettings.VESSEL_SPAWN_DISTANCE : BDArmorySettings.VESSEL_SPAWN_DISTANCE_FACTOR, BDArmorySettings.VESSEL_SPAWN_DISTANCE_TOGGLE, - BDArmorySettings.VESSEL_SPAWN_EASE_IN_SPEED, true, // Kill everything first. BDArmorySettings.VESSEL_SPAWN_REASSIGN_TEAMS, // Assign teams. 0, // Number of teams. @@ -178,17 +180,16 @@ public bool Generate(string folder, int numberOfRounds, int vesselsPerHeat, int Debug.Log($"[BDArmory.BDATournament]: " + message); BDACompetitionMode.Instance.competitionStatus.Add(message); // Generate all combinations of vessels for a round. - var heatList = new List(); + var heatList = new List(); foreach (var combination in Combinations(vesselCount, vesselsPerHeat)) { - heatList.Add(new SpawnConfig( + heatList.Add(new CircularSpawnConfig( BDArmorySettings.VESSEL_SPAWN_WORLDINDEX, BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.x, BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.y, - BDArmorySettings.VESSEL_SPAWN_ALTITUDE, + BDArmorySettings.VESSEL_SPAWN_ALTITUDE_, BDArmorySettings.VESSEL_SPAWN_DISTANCE_TOGGLE ? BDArmorySettings.VESSEL_SPAWN_DISTANCE : BDArmorySettings.VESSEL_SPAWN_DISTANCE_FACTOR, BDArmorySettings.VESSEL_SPAWN_DISTANCE_TOGGLE, - BDArmorySettings.VESSEL_SPAWN_EASE_IN_SPEED, true, // Kill everything first. BDArmorySettings.VESSEL_SPAWN_REASSIGN_TEAMS, // Assign teams. 0, // Number of teams. @@ -202,12 +203,15 @@ public bool Generate(string folder, int numberOfRounds, int vesselsPerHeat, int for (int roundIndex = 0; roundIndex < numberOfRounds; ++roundIndex) { heatList.Shuffle(); // Randomise the playing order within each round. - rounds.Add(roundIndex, heatList.Select((heat, index) => new KeyValuePair(index, heat)).ToDictionary(kvp => kvp.Key, kvp => kvp.Value)); + rounds.Add(roundIndex, heatList.Select((heat, index) => new KeyValuePair(index, heat)).ToDictionary(kvp => kvp.Key, kvp => kvp.Value)); } break; } default: - throw new ArgumentOutOfRangeException("tournamentStyle", "Invalid tournament style value - not implemented."); + { + BDACompetitionMode.Instance.competitionStatus.Add("Tournament style not implemented yet for FFA."); + throw new ArgumentOutOfRangeException("tournamentStyle", "Invalid tournament style value - not implemented."); + } } teamFiles = null; // Clear the teams lists. return true; @@ -228,8 +232,8 @@ public bool Generate(string folder, int numberOfRounds, int teamsPerHeat, int ve tournamentID = (uint)DateTime.UtcNow.Subtract(new DateTime(2020, 1, 1)).TotalSeconds; savegame = HighLogic.SaveFolder; tournamentType = TournamentType.Teams; - var abs_folder = Path.Combine(KSPUtil.ApplicationRootPath, "AutoSpawn", folder); - if (!Directory.Exists(abs_folder)) + var absFolder = Path.Combine(KSPUtil.ApplicationRootPath, "AutoSpawn", folder); + if (!Directory.Exists(absFolder)) { message = "Tournament folder (" + folder + ") containing craft files or team folders does not exist."; BDACompetitionMode.Instance.competitionStatus.Add(message); @@ -238,7 +242,7 @@ public bool Generate(string folder, int numberOfRounds, int teamsPerHeat, int ve } if (numberOfTeams > 1) // Make teams from the files in the spawn folder. { - craftFiles = Directory.GetFiles(abs_folder, "*.craft").ToList(); + craftFiles = Directory.GetFiles(absFolder, "*.craft").ToList(); if (craftFiles.Count < numberOfTeams) { message = "Insufficient vessels in AutoSpawn" + (!string.IsNullOrEmpty(folder) ? "/" + folder : "") + " to make " + numberOfTeams + " teams."; @@ -260,11 +264,19 @@ public bool Generate(string folder, int numberOfRounds, int teamsPerHeat, int ve } else // Make teams from the folders under the spawn folder. { - var teamDirs = Directory.GetDirectories(abs_folder); - if (teamDirs.Length == 0) // Make teams from each vessel in the spawn folder. + var teamDirs = Directory.GetDirectories(absFolder); + if (tournamentStyle == 2) // If it's a gauntlet tournament, ignore the opponents folder if it's in the main folder. + { + var opponentFolder = Path.GetFileName(BDArmorySettings.VESSEL_SPAWN_GAUNTLET_OPPONENTS_FILES_LOCATION.TrimEnd(new char[] { Path.DirectorySeparatorChar })); + if (teamDirs.Select(d => Path.GetFileName(d)).Contains(opponentFolder)) + { + teamDirs = teamDirs.Where(d => Path.GetFileName(d) != opponentFolder).ToArray(); + } + } + if (teamDirs.Length < (tournamentStyle != 2 ? 2 : 1)) // Make teams from each vessel in the spawn folder. Allow for a single subfolder for putting bad craft or other tmp things in, unless it's a gauntlet competition. { numberOfTeams = -1; // Flag for treating craft files as folder names. - craftFiles = Directory.GetFiles(abs_folder, "*.craft").ToList(); + craftFiles = Directory.GetFiles(absFolder, "*.craft").ToList(); teamFiles = craftFiles.Select(f => new List { f }).ToList(); } else @@ -282,7 +294,7 @@ public bool Generate(string folder, int numberOfRounds, int teamsPerHeat, int ve } } vesselCount = craftFiles.Count; - if (teamFiles.Count < 2) + if (teamFiles.Count < (tournamentStyle != 2 ? 2 : 1)) { message = $"Insufficient {(numberOfTeams != 1 ? "craft files" : "folders")} in '{Path.Combine("AutoSpawn", folder)}' to generate a tournament."; if (BDACompetitionMode.Instance) BDACompetitionMode.Instance.competitionStatus.Add(message); @@ -290,7 +302,7 @@ public bool Generate(string folder, int numberOfRounds, int teamsPerHeat, int ve return false; } teamCount = teamFiles.Count; - teamsPerHeat = Mathf.Clamp(teamsPerHeat, 2, teamFiles.Count); + teamsPerHeat = Mathf.Clamp(teamsPerHeat, (tournamentStyle != 2 ? 2 : 1), teamFiles.Count); this.teamsPerHeat = teamsPerHeat; this.vesselsPerTeam = vesselsPerTeam; fullTeams = BDArmorySettings.TOURNAMENT_FULL_TEAMS; @@ -298,7 +310,7 @@ public bool Generate(string folder, int numberOfRounds, int teamsPerHeat, int ve teamSpawnQueues.Clear(); int fullHeatCount = teamFiles.Count / teamsPerHeat; - rounds = new Dictionary>(); + rounds = new Dictionary>(); switch (tournamentStyle) { case 0: // RNG @@ -313,18 +325,17 @@ public bool Generate(string folder, int numberOfRounds, int teamsPerHeat, int ve int count = 0; var selectedTeams = teamsIndex.Take(teamsThisHeat).ToList(); var selectedCraft = SelectTeamCraft(selectedTeams, vesselsPerTeam); - rounds.Add(rounds.Count, new Dictionary()); + rounds.Add(rounds.Count, new Dictionary()); int heatIndex = 0; while (selectedTeams.Count > 0) { - rounds[roundIndex].Add(rounds[roundIndex].Count, new SpawnConfig( + rounds[roundIndex].Add(rounds[roundIndex].Count, new CircularSpawnConfig( BDArmorySettings.VESSEL_SPAWN_WORLDINDEX, BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.x, BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.y, - BDArmorySettings.VESSEL_SPAWN_ALTITUDE, + BDArmorySettings.VESSEL_SPAWN_ALTITUDE_, BDArmorySettings.VESSEL_SPAWN_DISTANCE_TOGGLE ? BDArmorySettings.VESSEL_SPAWN_DISTANCE : BDArmorySettings.VESSEL_SPAWN_DISTANCE_FACTOR, BDArmorySettings.VESSEL_SPAWN_DISTANCE_TOGGLE, - BDArmorySettings.VESSEL_SPAWN_EASE_IN_SPEED, true, // Kill everything first. BDArmorySettings.VESSEL_SPAWN_REASSIGN_TEAMS, // Assign teams. numberOfTeams, // Number of teams indicator. @@ -352,18 +363,17 @@ public bool Generate(string folder, int numberOfRounds, int teamsPerHeat, int ve // Populate the rounds. for (int roundIndex = 0; roundIndex < numberOfRounds; ++roundIndex) { - var heatList = new List(); + var heatList = new List(); foreach (var combination in combinations) { var selectedCraft = SelectTeamCraft(combination.Select(i => teamsIndex[i]).ToList(), vesselsPerTeam); // Vessel selection for a team can vary between rounds if the number of vessels in a team doesn't match the vesselsPerTeam parameter. - heatList.Add(new SpawnConfig( + heatList.Add(new CircularSpawnConfig( BDArmorySettings.VESSEL_SPAWN_WORLDINDEX, BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.x, BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.y, - BDArmorySettings.VESSEL_SPAWN_ALTITUDE, + BDArmorySettings.VESSEL_SPAWN_ALTITUDE_, BDArmorySettings.VESSEL_SPAWN_DISTANCE_TOGGLE ? BDArmorySettings.VESSEL_SPAWN_DISTANCE : BDArmorySettings.VESSEL_SPAWN_DISTANCE_FACTOR, BDArmorySettings.VESSEL_SPAWN_DISTANCE_TOGGLE, - BDArmorySettings.VESSEL_SPAWN_EASE_IN_SPEED, true, // Kill everything first. BDArmorySettings.VESSEL_SPAWN_REASSIGN_TEAMS, // Assign teams. numberOfTeams, // Number of teams indicator. @@ -374,8 +384,93 @@ public bool Generate(string folder, int numberOfRounds, int teamsPerHeat, int ve )); } heatList.Shuffle(); // Randomise the playing order within each round. - rounds.Add(roundIndex, heatList.Select((heat, index) => new KeyValuePair(index, heat)).ToDictionary(kvp => kvp.Key, kvp => kvp.Value)); + rounds.Add(roundIndex, heatList.Select((heat, index) => new KeyValuePair(index, heat)).ToDictionary(kvp => kvp.Key, kvp => kvp.Value)); + } + break; + } + case 2: // Gauntlet + { + // Gauntlet is like N-choose-2K except that it's selecting K teams from the main folder and K teams from the opponents (e.g., 2v2, 3v3, 2v3, (2v2)v(2v2), (2v3)v(3v2), etc.) + #region Opponent config + var opponentFolder = Path.Combine("AutoSpawn", BDArmorySettings.VESSEL_SPAWN_GAUNTLET_OPPONENTS_FILES_LOCATION); + var opponentAbsFolder = Path.Combine(KSPUtil.ApplicationRootPath, opponentFolder); + if (!Directory.Exists(opponentAbsFolder)) + { + message = "Opponents folder (" + opponentFolder + ") containing craft files or team folders does not exist."; + BDACompetitionMode.Instance.competitionStatus.Add(message); + Debug.Log("[BDArmory.BDATournament]: " + message); + return false; + } + var opponentTeamDirs = Directory.GetDirectories(opponentAbsFolder); + List opponentCraftFiles; + if (opponentTeamDirs.Length < 1) // Make teams from each vessel in the opponents folder. + { + opponentCraftFiles = Directory.GetFiles(opponentAbsFolder, "*.craft").ToList(); + opponentTeamFiles = opponentCraftFiles.Select(f => new List { f }).ToList(); + } + else + { + opponentTeamFiles = new List>(); + foreach (var teamDir in opponentTeamDirs) + { + var currentTeamFiles = Directory.GetFiles(teamDir, "*.craft").ToList(); + if (currentTeamFiles.Count > 0) + opponentTeamFiles.Add(currentTeamFiles); + } + foreach (var team in opponentTeamFiles) + team.Shuffle(); + opponentCraftFiles = opponentTeamFiles.SelectMany(v => v).ToList(); + } + if (opponentTeamFiles.Count < 1) + { + message = $"Insufficient {(opponentTeamDirs.Length < 1 ? "craft files" : "folders")} in '{opponentFolder}' to generate a gauntlet tournament."; + if (BDACompetitionMode.Instance) BDACompetitionMode.Instance.competitionStatus.Add(message); + Debug.Log("[BDArmory.BDATournament]: " + message); + return false; + } + var opponentTeamCount = opponentTeamFiles.Count; + var opponentTeamsIndex = Enumerable.Range(0, opponentTeamCount).ToList(); + #endregion + + #region Tournament generation + var nCr = N_Choose_K(teamCount, teamsPerHeat) * N_Choose_K(opponentTeamCount, BDArmorySettings.TOURNAMENT_OPPONENT_TEAMS_PER_HEAT); + message = $"Generating a gauntlet style tournament for {teamCount} teams in AutoSpawn{(folder == "" ? "" : "/" + folder)} and {opponentTeamCount} opponent teams in {opponentFolder} with {BDArmorySettings.TOURNAMENT_OPPONENT_TEAMS_PER_HEAT} teams per heat and {numberOfRounds} rounds. This requires {numberOfRounds * nCr} heats."; + Debug.Log($"[BDArmory.BDATournament]: " + message); + BDACompetitionMode.Instance.competitionStatus.Add(message); + // Generate all combinations of teams for a round. + var combinations = Combinations(teamCount, teamsPerHeat); + var opponentCombinations = Combinations(opponentTeamCount, BDArmorySettings.TOURNAMENT_OPPONENT_TEAMS_PER_HEAT); + // Populate the rounds. + for (int roundIndex = 0; roundIndex < numberOfRounds; ++roundIndex) + { + var heatList = new List(); + foreach (var combination in combinations) + { + var selectedCraft = SelectTeamCraft(combination.Select(i => teamsIndex[i]).ToList(), vesselsPerTeam); // Vessel selection for a team can vary between rounds if the number of vessels in a team doesn't match the vesselsPerTeam parameter. + foreach (var opponentCombination in opponentCombinations) + { + var selectedOpponentCraft = SelectTeamCraft(opponentCombination.Select(i => opponentTeamsIndex[i]).ToList(), BDArmorySettings.TOURNAMENT_OPPONENT_VESSELS_PER_TEAM, true); // Vessel selection for a team can vary between rounds if the number of vessels in a team doesn't match the vesselsPerTeam parameter. + heatList.Add(new CircularSpawnConfig( + BDArmorySettings.VESSEL_SPAWN_WORLDINDEX, + BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.x, + BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.y, + BDArmorySettings.VESSEL_SPAWN_ALTITUDE_, + BDArmorySettings.VESSEL_SPAWN_DISTANCE_TOGGLE ? BDArmorySettings.VESSEL_SPAWN_DISTANCE : BDArmorySettings.VESSEL_SPAWN_DISTANCE_FACTOR, + BDArmorySettings.VESSEL_SPAWN_DISTANCE_TOGGLE, + true, // Kill everything first. + BDArmorySettings.VESSEL_SPAWN_REASSIGN_TEAMS, // Assign teams. + numberOfTeams, // Number of teams indicator. (Should be -1 for gauntlets for now.) + null, //selectedCraft.Select(c => c.Count).ToList(), // Not used here. + selectedCraft.Concat(selectedOpponentCraft).ToList(), // List of lists of vessels. For splitting specific vessels into specific teams. + null, // No folder, we're going to specify the craft files. + null // No list of craft files, we've specified them directly in selectedCraft. + )); + } + } + heatList.Shuffle(); // Randomise the playing order within each round. + rounds.Add(roundIndex, heatList.Select((heat, index) => new KeyValuePair(index, heat)).ToDictionary(kvp => kvp.Key, kvp => kvp.Value)); } + #endregion break; } default: @@ -384,45 +479,49 @@ public bool Generate(string folder, int numberOfRounds, int teamsPerHeat, int ve return true; } - List> SelectTeamCraft(List selectedTeams, int vesselsPerTeam) + List> SelectTeamCraft(List selectedTeams, int vesselsPerTeam, bool opponentQueue = false) { - if (teamSpawnQueues.Count == 0) // Set up the spawn queues. + // Get the right spawn queues and file lists. + var spawnQueues = opponentQueue ? opponentTeamSpawnQueues : teamSpawnQueues; + var teams = opponentQueue ? opponentTeamFiles : teamFiles; + + if (spawnQueues.Count == 0) // Set up the spawn queues if needed. { - foreach (var teamIndex in teamFiles) - teamSpawnQueues.Add(new Queue()); + foreach (var teamIndex in teams) + spawnQueues.Add(new Queue()); } List> selectedCraft = new List>(); List currentTeam = new List(); foreach (var index in selectedTeams) { - if (teamSpawnQueues[index].Count < vesselsPerTeam) + if (spawnQueues[index].Count < vesselsPerTeam) { // First append craft files that aren't already in the queue. - var craftToAdd = teamFiles[index].Where(c => !teamSpawnQueues[index].Contains(c)).ToList(); + var craftToAdd = teams[index].Where(c => !spawnQueues[index].Contains(c)).ToList(); craftToAdd.Shuffle(); foreach (var craft in craftToAdd) { - teamSpawnQueues[index].Enqueue(craft); + spawnQueues[index].Enqueue(craft); } if (BDArmorySettings.TOURNAMENT_FULL_TEAMS) { // Then continue to fill the queue with craft files until we have enough. - while (teamSpawnQueues[index].Count < vesselsPerTeam) + while (spawnQueues[index].Count < vesselsPerTeam) { - craftToAdd = teamFiles[index].ToList(); + craftToAdd = teams[index].ToList(); craftToAdd.Shuffle(); foreach (var craft in craftToAdd) { - teamSpawnQueues[index].Enqueue(craft); + spawnQueues[index].Enqueue(craft); } } } } currentTeam.Clear(); - while (currentTeam.Count < vesselsPerTeam && teamSpawnQueues[index].Count > 0) + while (currentTeam.Count < vesselsPerTeam && spawnQueues[index].Count > 0) { - currentTeam.Add(teamSpawnQueues[index].Dequeue()); + currentTeam.Add(spawnQueues[index].Dequeue()); } selectedCraft.Add(currentTeam.ToList()); } @@ -484,7 +583,7 @@ public bool LoadState(string stateFile) vesselsPerTeam = data.vesselsPerTeam; fullTeams = data.fullTeams; tournamentType = data.tournamentType; - rounds = new Dictionary>(); + rounds = new Dictionary>(); completed = new Dictionary>(); for (int i = 1; i < strings.Length; ++i) { @@ -493,15 +592,14 @@ public bool LoadState(string stateFile) var roundConfig = JsonUtility.FromJson(strings[i]); if (!strings[i].Contains("worldIndex")) roundConfig.worldIndex = 1; // Default old tournament states to be on Kerbin. roundConfig.DeserializeTeams(); - if (!rounds.ContainsKey(roundConfig.round)) rounds.Add(roundConfig.round, new Dictionary()); - rounds[roundConfig.round].Add(roundConfig.heat, new SpawnConfig( + if (!rounds.ContainsKey(roundConfig.round)) rounds.Add(roundConfig.round, new Dictionary()); + rounds[roundConfig.round].Add(roundConfig.heat, new CircularSpawnConfig( roundConfig.worldIndex, roundConfig.latitude, roundConfig.longitude, roundConfig.altitude, roundConfig.distance, roundConfig.absDistanceOrFactor, - roundConfig.easeInSpeed, roundConfig.killEverythingFirst, roundConfig.assignTeams, roundConfig.numberOfTeams, @@ -690,7 +788,7 @@ public void SetupTournament(string folder, int rounds, int vesselsPerHeat = 0, i } } if (stateFile != "") this.stateFile = stateFile; - if ((BDArmorySettings.WAYPOINTS_MODE || (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 50)) && BDArmorySettings.WAYPOINTS_ONE_AT_A_TIME) vesselsPerHeat = 1; // Override vessels per heat. + if ((BDArmorySettings.WAYPOINTS_MODE || (BDArmorySettings.RUNWAY_PROJECT && (BDArmorySettings.RUNWAY_PROJECT_ROUND == 50 || BDArmorySettings.RUNWAY_PROJECT_ROUND == 55))) && BDArmorySettings.WAYPOINTS_ONE_AT_A_TIME) vesselsPerHeat = 1; // Override vessels per heat. tournamentState = new TournamentState(); if (numberOfTeams == 0) // FFA { @@ -722,6 +820,7 @@ public void RunTournament() if (runTournamentCoroutine != null) StopCoroutine(runTournamentCoroutine); runTournamentCoroutine = StartCoroutine(RunTournamentCoroutine()); + if (BDArmorySettings.AUTO_DISABLE_UI) SetGameUI(false); } public void StopTournament() @@ -732,6 +831,7 @@ public void StopTournament() runTournamentCoroutine = null; } tournamentStatus = heatsRemaining > 0 ? TournamentStatus.Stopped : TournamentStatus.Completed; + if (BDArmorySettings.AUTO_DISABLE_UI) SetGameUI(true); } IEnumerator RunTournamentCoroutine() @@ -751,14 +851,15 @@ IEnumerator RunTournamentCoroutine() Debug.Log("[BDArmory.BDATournament]: " + message); int attempts = 0; + bool unrecoverable = false; competitionStarted = false; while (!competitionStarted && attempts++ < 3) // 3 attempts is plenty { tournamentStatus = TournamentStatus.Running; - if (BDArmorySettings.WAYPOINTS_MODE || (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 50)) + if (BDArmorySettings.WAYPOINTS_MODE || (BDArmorySettings.RUNWAY_PROJECT && (BDArmorySettings.RUNWAY_PROJECT_ROUND == 50 || BDArmorySettings.RUNWAY_PROJECT_ROUND == 55))) yield return ExecuteWaypointHeat(roundIndex, heatIndex); else - yield return ExecuteHeat(roundIndex, heatIndex); + yield return ExecuteHeat(roundIndex, heatIndex, attempts == 3 && BDArmorySettings.COMPETITION_START_DESPITE_FAILURES); // On the third attempt, start despite failures if the option is set. if (!competitionStarted) switch (CircularSpawning.Instance.spawnFailureReason) { @@ -776,15 +877,23 @@ IEnumerator RunTournamentCoroutine() BDACompetitionMode.Instance.competitionStatus.Add("Failed to start heat due to " + CircularSpawning.Instance.spawnFailureReason + ", trying again."); attempts = Math.Max(attempts, 2); // Try only once more. break; + case SpawnFailureReason.DependencyIssues: + message = $"Failed to start heat due to {CircularSpawning.Instance.spawnFailureReason}, aborting. Make sure dependencies are installed and enabled, then revert to launch and try again."; + BDACompetitionMode.Instance.competitionStatus.Add(message); + Debug.LogWarning($"[BDArmory.BDATournament]: {message}"); + attempts = 3; + unrecoverable = true; + break; default: // Spawning is unrecoverable. BDACompetitionMode.Instance.competitionStatus.Add("Failed to start heat due to " + CircularSpawning.Instance.spawnFailureReason + ", aborting."); attempts = 3; + unrecoverable = true; break; } } if (!competitionStarted) { - message = "Failed to run heat after 3 spawning attempts, failure reasons: " + CircularSpawning.Instance.spawnFailureReason + ", " + BDACompetitionMode.Instance.competitionStartFailureReason + ". Stopping tournament. Please fix the failure reason before continuing the tournament."; + message = $"Failed to run heat {(unrecoverable?"due to unrecoverable error": $"after 3 spawning attempts")}, failure reasons: " + CircularSpawning.Instance.spawnFailureReason + ", " + BDACompetitionMode.Instance.competitionStartFailureReason + ". Stopping tournament. Please fix the failure reason before continuing the tournament."; Debug.Log("[BDArmory.BDATournament]: " + message); BDACompetitionMode.Instance.competitionStatus.Add(message); tournamentStatus = TournamentStatus.Stopped; @@ -817,7 +926,7 @@ IEnumerator RunTournamentCoroutine() message = "All heats in round " + roundIndex + " have been run."; BDACompetitionMode.Instance.competitionStatus.Add(message); Debug.Log("[BDArmory.BDATournament]: " + message); - if (BDArmorySettings.WAYPOINTS_MODE || (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 50)) + if (BDArmorySettings.WAYPOINTS_MODE || (BDArmorySettings.RUNWAY_PROJECT && (BDArmorySettings.RUNWAY_PROJECT_ROUND == 50 || BDArmorySettings.RUNWAY_PROJECT_ROUND == 55))) { /* commented out until this is made functional foreach (var tracer in WaypointFollowingStrategy.Ghosts) //clear and reset vessel ghosts each new Round @@ -852,12 +961,13 @@ IEnumerator RunTournamentCoroutine() BDACompetitionMode.Instance.competitionStatus.Add(message); Debug.Log("[BDArmory.BDATournament]: " + message); tournamentStatus = TournamentStatus.Completed; + if (BDArmorySettings.AUTO_DISABLE_UI) SetGameUI(true); var partialStatePath = Path.ChangeExtension(Path.Combine(Path.GetDirectoryName(TournamentState.defaultStateFile), "Unfinished Tournaments", Path.GetFileName(stateFile)), $".state-{tournamentID}"); if (File.Exists(partialStatePath)) File.Delete(partialStatePath); // Remove the now completed tournament state file. if (BDArmorySettings.AUTO_RESUME_TOURNAMENT && BDArmorySettings.AUTO_QUIT_AT_END_OF_TOURNAMENT && TournamentAutoResume.Instance != null) { - TournamentAutoResume.Instance.AutoQuit(5); + TournamentAutoResume.AutoQuit(5); message = "Quitting KSP in 5s due to reaching the end of a tournament."; BDACompetitionMode.Instance.competitionStatus.Add(message); Debug.LogWarning("[BDArmory.BDATournament]: " + message); @@ -869,23 +979,12 @@ IEnumerator ExecuteWaypointHeat(int roundIndex, int heatIndex) { if (TournamentCoordinator.Instance.IsRunning) TournamentCoordinator.Instance.Stop(); var spawnConfig = tournamentState.rounds[roundIndex][heatIndex]; - spawnConfig.worldIndex = 1; - spawnConfig.latitude = 27.97f; - spawnConfig.longitude = -39.35f; + spawnConfig.worldIndex = WaypointCourses.CourseLocations[BDArmorySettings.WAYPOINT_COURSE_INDEX].worldIndex; + spawnConfig.latitude = WaypointCourses.CourseLocations[BDArmorySettings.WAYPOINT_COURSE_INDEX].spawnPoint.x; + spawnConfig.longitude = WaypointCourses.CourseLocations[BDArmorySettings.WAYPOINT_COURSE_INDEX].spawnPoint.y; TournamentCoordinator.Instance.Configure(new SpawnConfigStrategy(spawnConfig), - new WaypointFollowingStrategy( - new List { - new WaypointFollowingStrategy.Waypoint(28.33f, -39.11f, BDArmorySettings.WAYPOINTS_ALTITUDE), - new WaypointFollowingStrategy.Waypoint(28.83f, -38.06f, BDArmorySettings.WAYPOINTS_ALTITUDE), - new WaypointFollowingStrategy.Waypoint(29.54f, -38.68f, BDArmorySettings.WAYPOINTS_ALTITUDE), - new WaypointFollowingStrategy.Waypoint(30.15f, -38.6f, BDArmorySettings.WAYPOINTS_ALTITUDE), - new WaypointFollowingStrategy.Waypoint(30.83f, -38.87f, BDArmorySettings.WAYPOINTS_ALTITUDE), - new WaypointFollowingStrategy.Waypoint(30.73f, -39.6f, BDArmorySettings.WAYPOINTS_ALTITUDE), - new WaypointFollowingStrategy.Waypoint(30.9f, -40.23f, BDArmorySettings.WAYPOINTS_ALTITUDE), - new WaypointFollowingStrategy.Waypoint(30.83f, -41.26f, BDArmorySettings.WAYPOINTS_ALTITUDE) - } - ), + new WaypointFollowingStrategy(WaypointCourses.CourseLocations[BDArmorySettings.WAYPOINT_COURSE_INDEX].waypoints), CircularSpawning.Instance ); @@ -895,7 +994,7 @@ IEnumerator ExecuteWaypointHeat(int roundIndex, int heatIndex) yield return new WaitWhile(() => TournamentCoordinator.Instance.IsRunning); } - IEnumerator ExecuteHeat(int roundIndex, int heatIndex) + IEnumerator ExecuteHeat(int roundIndex, int heatIndex, bool startDespiteFailures = false) { CircularSpawning.Instance.SpawnAllVesselsOnce(tournamentState.rounds[roundIndex][heatIndex]); while (CircularSpawning.Instance.vesselsSpawning) @@ -918,20 +1017,20 @@ IEnumerator ExecuteHeat(int roundIndex, int heatIndex) case 44: BDACompetitionMode.Instance.StartRapidDeployment(0); break; - case 60: // FIXME temporary index, to be assigned later + case 53: BDACompetitionMode.Instance.StartRapidDeployment(0); break; default: - BDACompetitionMode.Instance.StartCompetitionMode(BDArmorySettings.COMPETITION_DISTANCE); + BDACompetitionMode.Instance.StartCompetitionMode(BDArmorySettings.COMPETITION_DISTANCE, startDespiteFailures); break; } } else - BDACompetitionMode.Instance.StartCompetitionMode(BDArmorySettings.COMPETITION_DISTANCE); + BDACompetitionMode.Instance.StartCompetitionMode(BDArmorySettings.COMPETITION_DISTANCE, startDespiteFailures); yield return new WaitForFixedUpdate(); // Give the competition start a frame to get going. // start timer coroutine for the duration specified in settings UI - var duration = Core.BDArmorySettings.COMPETITION_DURATION * 60f; + var duration = BDArmorySettings.COMPETITION_DURATION * 60f; message = "Starting " + (duration > 0 ? "a " + duration.ToString("F0") + "s" : "an unlimited") + " duration competition."; Debug.Log("[BDArmory.BDATournament]: " + message); BDACompetitionMode.Instance.competitionStatus.Add(message); @@ -967,7 +1066,7 @@ IEnumerator WarpAhead(double warpTimeBetweenHeats) int tries = 0; do { - spawnProbe = SpawnUtils.SpawnSpawnProbe(); + spawnProbe = VesselSpawner.SpawnSpawnProbe(); yield return new WaitWhile(() => spawnProbe != null && (!spawnProbe.loaded || spawnProbe.packed)); while (spawnProbe != null && FlightGlobals.ActiveVessel != spawnProbe) { @@ -984,7 +1083,7 @@ IEnumerator WarpAhead(double warpTimeBetweenHeats) } var up = VectorUtils.GetUpDirection(spawnProbe.transform.position); var refDirection = Math.Abs(Vector3.Dot(Vector3.up, up)) < 0.71f ? Vector3.up : Vector3.forward; // Avoid that the reference direction is colinear with the local surface normal. - spawnProbe.SetPosition(spawnProbe.transform.position - Utils.GetRadarAltitudeAtPos(spawnProbe.transform.position) * up); + spawnProbe.SetPosition(spawnProbe.transform.position - BodyUtils.GetRadarAltitudeAtPos(spawnProbe.transform.position) * up); if (spawnProbe.altitude > 0) spawnProbe.Landed = true; else spawnProbe.Splashed = true; spawnProbe.SetWorldVelocity(Vector3d.zero); // Set the velocity to zero so that warp goes in high mode. @@ -1017,7 +1116,7 @@ IEnumerator WarpAhead(double warpTimeBetweenHeats) TimeWarp.fetch.CancelAutoWarp(); TimeWarp.SetRate(0, true, false); while (TimeWarp.CurrentRate > 1) yield return null; // Wait for the warping to stop. - spawnProbe.SetPosition(spawnProbe.transform.position - Utils.GetRadarAltitudeAtPos(spawnProbe.transform.position) * up); + spawnProbe.SetPosition(spawnProbe.transform.position - BodyUtils.GetRadarAltitudeAtPos(spawnProbe.transform.position) * up); if (spawnProbe.altitude > 0) spawnProbe.Landed = true; else spawnProbe.Splashed = true; spawnProbe.SetWorldVelocity(Vector3d.zero); // Set the velocity to zero so that warp goes in high mode. @@ -1041,6 +1140,17 @@ IEnumerator WarpAhead(double warpTimeBetweenHeats) warpingInProgress = false; } + + void SetGameUI(bool enable) + { if (isActiveAndEnabled) StartCoroutine(SetGameUIWorker(enable)); } + IEnumerator SetGameUIWorker(bool enable) + { + // On first entering flight mode, KSP issues a couple of ShowUI after a few seconds. WTF KSP devs?! + yield return new WaitUntil(() => BDACompetitionMode.Instance is not null && (BDACompetitionMode.Instance.competitionStarting || BDACompetitionMode.Instance.competitionIsActive)); + // Also, triggering ShowUI/HideUI doesn't trigger the onShowUI/onHideUI events, so we need to fire them off ourselves. + if (enable) { KSP.UI.UIMasterController.Instance.ShowUI(); GameEvents.onShowUI.Fire(); } + else { KSP.UI.UIMasterController.Instance.HideUI(); GameEvents.onHideUI.Fire(); } + } } /// @@ -1103,7 +1213,7 @@ IEnumerator WaitForSettings() { yield return new WaitForSeconds(0.5f); var tic = Time.realtimeSinceStartup; - yield return new WaitUntil(() => (BDArmorySettings.ready || Time.realtimeSinceStartup - tic > 10)); // Wait until the settings are ready or timed out. + yield return new WaitUntil(() => (BDArmorySettings.ready || Time.realtimeSinceStartup - tic > 30)); // Wait until the settings are ready or timed out. Debug.Log($"[BDArmory.BDATournament]: BDArmory settings loaded, auto-load to KSC: {BDArmorySettings.AUTO_LOAD_TO_KSC}, auto-resume tournaments: {BDArmorySettings.AUTO_RESUME_TOURNAMENT}, auto-resume evolution: {BDArmorySettings.AUTO_RESUME_EVOLUTION}."); if (BDArmorySettings.AUTO_RESUME_TOURNAMENT || BDArmorySettings.AUTO_RESUME_EVOLUTION || BDArmorySettings.AUTO_LOAD_TO_KSC) { yield return StartCoroutine(AutoResumeTournament()); } @@ -1137,7 +1247,7 @@ IEnumerator AutoResumeTournament() if (!(resumingEvolution || resumingTournament)) yield break; // Just load to the KSC. // Switch to flight mode. sceneLoaded = false; - FlightDriver.StartWithNewLaunch(SpawnUtils.spawnProbeLocation, "GameData/Squad/Flags/default.png", FlightDriver.LaunchSiteName, new VesselCrewManifest()); // This triggers an error for SpaceCenterCamera2, but I don't see how to fix it and it doesn't appear to be harmful. + FlightDriver.StartWithNewLaunch(VesselSpawner.spawnProbeLocation, "GameData/Squad/Flags/default.png", FlightDriver.LaunchSiteName, new VesselCrewManifest()); // This triggers an error for SpaceCenterCamera2, but I don't see how to fix it and it doesn't appear to be harmful. tic = Time.time; yield return new WaitUntil(() => (sceneLoaded || Time.time - tic > 10)); if (!sceneLoaded) { Debug.Log("[BDArmory.BDATournament]: Failed to load flight scene."); yield break; } @@ -1149,7 +1259,7 @@ IEnumerator AutoResumeTournament() yield return new WaitWhile(() => (BDAModuleEvolution.Instance == null && Time.time - tic < 10)); // Wait for the tournament to be loaded or time out. if (BDAModuleEvolution.Instance == null) yield break; BDArmorySetup.windowBDAToolBarEnabled = true; - BDArmorySetup.Instance.showVesselSwitcherGUI = true; + LoadedVesselSwitcher.Instance.SetVisible(true); BDArmorySetup.Instance.showEvolutionGUI = true; BDAModuleEvolution.Instance.ResumeEvolution(evolutionState); } @@ -1173,8 +1283,8 @@ IEnumerator AutoResumeTournament() yield return new WaitWhile(() => ((BDATournament.Instance == null || BDATournament.Instance.tournamentID == 0) && Time.time - tic < 10)); // Wait for the tournament to be loaded or time out. if (BDATournament.Instance == null || BDATournament.Instance.tournamentID == 0) yield break; BDArmorySetup.windowBDAToolBarEnabled = true; - BDArmorySetup.Instance.showVesselSwitcherGUI = true; - BDArmorySetup.Instance.showVesselSpawnerGUI = true; + LoadedVesselSwitcher.Instance.SetVisible(true); + VesselSpawnerWindow.Instance.SetVisible(true); BDATournament.Instance.RunTournament(); } } @@ -1327,7 +1437,7 @@ public bool CheckMemoryUsage() return false; } - public void AutoQuit(float delay = 1) => StartCoroutine(AutoQuitCoroutine(delay)); + public static void AutoQuit(float delay = 1) => Instance.StartCoroutine(Instance.AutoQuitCoroutine(delay)); /// /// Automatically quit KSP after a delay. @@ -1339,6 +1449,7 @@ IEnumerator AutoQuitCoroutine(float delay = 1) yield return new WaitForSeconds(delay); HighLogic.LoadScene(GameScenes.MAINMENU); yield return new WaitForSeconds(0.5f); // Pause on the Main Menu a moment, then quit. + Debug.Log("[BDArmory.BDATournament]: Quitting KSP."); Application.Quit(); } } diff --git a/BDArmory/Misc/BDTeam.cs b/BDArmory/Competition/BDTeam.cs similarity index 93% rename from BDArmory/Misc/BDTeam.cs rename to BDArmory/Competition/BDTeam.cs index e79a2c778..5014f9392 100644 --- a/BDArmory/Misc/BDTeam.cs +++ b/BDArmory/Competition/BDTeam.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Generic; -using BDArmory.UI; using UnityEngine; -namespace BDArmory.Misc +using BDArmory.UI; +using BDArmory.Utils; + +namespace BDArmory.Competition { [Serializable] public class BDTeam @@ -55,7 +57,7 @@ public static BDTeam Deserialize(string teamString) return BDTeam.Get("B"); try { - BDTeam team = UnityEngine.JsonUtility.FromJson(Utils.JsonDecompat(teamString)); + BDTeam team = UnityEngine.JsonUtility.FromJson(OtherUtils.JsonDecompat(teamString)); if (!BDArmorySetup.Instance.Teams.ContainsKey(team.Name)) { BDArmorySetup.Instance.Teams.Add(team.Name, team); @@ -71,7 +73,7 @@ public static BDTeam Deserialize(string teamString) public string Serialize() { - return Utils.JsonCompat(UnityEngine.JsonUtility.ToJson(this)); + return OtherUtils.JsonCompat(UnityEngine.JsonUtility.ToJson(this)); } public override int GetHashCode() => Name.GetHashCode(); diff --git a/BDArmory/Competition/OrchestrationStrategies/RankedFreeForAllStrategy.cs b/BDArmory/Competition/OrchestrationStrategies/RankedFreeForAllStrategy.cs index 0751b88e9..1a59ce0b6 100644 --- a/BDArmory/Competition/OrchestrationStrategies/RankedFreeForAllStrategy.cs +++ b/BDArmory/Competition/OrchestrationStrategies/RankedFreeForAllStrategy.cs @@ -1,10 +1,7 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using BDArmory.Control; -using BDArmory.Core; +using System.Collections; using UnityEngine; + +using BDArmory.Settings; using BDArmory.Competition.RemoteOrchestration; using static BDArmory.Competition.RemoteOrchestration.BDAScoreService; @@ -41,11 +38,31 @@ private IEnumerator ExecuteHeat(string hash, HeatModel model) service.ClearScores(); service.status = StatusType.RunningHeat; - BDACompetitionMode.Instance.StartCompetitionMode(BDArmorySettings.COMPETITION_DISTANCE); + if (BDArmorySettings.RUNWAY_PROJECT) + { + switch (BDArmorySettings.RUNWAY_PROJECT_ROUND) + { + case 33: + BDACompetitionMode.Instance.StartRapidDeployment(0, tag: $"{model.competition_id}-{model.stage}-{model.order}"); + break; + case 44: + BDACompetitionMode.Instance.StartRapidDeployment(0, tag: $"{model.competition_id}-{model.stage}-{model.order}"); + break; + case 53: + BDACompetitionMode.Instance.StartRapidDeployment(0, tag: $"{model.competition_id}-{model.stage}-{model.order}"); + break; + default: + BDACompetitionMode.Instance.StartCompetitionMode(BDArmorySettings.COMPETITION_DISTANCE, BDArmorySettings.COMPETITION_START_DESPITE_FAILURES, tag: $"{model.competition_id}-{model.stage}-{model.order}"); + break; + } + } + else + BDACompetitionMode.Instance.StartCompetitionMode(BDArmorySettings.COMPETITION_DISTANCE, BDArmorySettings.COMPETITION_START_DESPITE_FAILURES, tag: $"{model.competition_id}-{model.stage}-{model.order}"); + //BDACompetitionMode.Instance.StartCompetitionMode(BDArmorySettings.COMPETITION_DISTANCE, BDArmorySettings.COMPETITION_START_DESPITE_FAILURES); yield return new WaitForFixedUpdate(); // Give the competition start a frame to get going. // start timer coroutine for the duration specified in settings UI - var duration = Core.BDArmorySettings.COMPETITION_DURATION * 60d; + var duration = BDArmorySettings.COMPETITION_DURATION * 60d; var message = "Starting " + (duration > 0 ? "a " + duration.ToString("F0") + "s" : "an unlimited") + " duration competition."; Debug.Log("[BDArmory.BDAScoreService]: " + message); BDACompetitionMode.Instance.competitionStatus.Add(message); diff --git a/BDArmory/Competition/OrchestrationStrategies/WaypointFollowingStrategy.cs b/BDArmory/Competition/OrchestrationStrategies/WaypointFollowingStrategy.cs index 5e653e04e..2b6afa9d5 100644 --- a/BDArmory/Competition/OrchestrationStrategies/WaypointFollowingStrategy.cs +++ b/BDArmory/Competition/OrchestrationStrategies/WaypointFollowingStrategy.cs @@ -2,35 +2,57 @@ using System.Collections; using System.Collections.Generic; using System.Linq; +using UnityEngine; + using BDArmory.Competition.RemoteOrchestration; +using BDArmory.Control; +using BDArmory.Damage; +using BDArmory.FX; +using BDArmory.GameModes.Waypoints; using BDArmory.Modules; -using BDArmory.Core; +using BDArmory.Settings; using BDArmory.UI; -using UnityEngine; -using BDArmory.Misc; -using System.IO; -using BDArmory.FX; -using BDArmory.Core.Module; +using BDArmory.Utils; namespace BDArmory.Competition.OrchestrationStrategies { public class WaypointFollowingStrategy : OrchestrationStrategy { + /* public class Waypoint { + //waypoint container class - holds coord data, scale, and WP name public float latitude; public float longitude; public float altitude; - public Waypoint(float latitude, float longitude, float altitude) //really, this should become the waypointmarker, and be a class that contains both a dataset (lat/long coords) and a togglable model + public string waypointName = "Waypoint"; + public double waypointScale = 500; + public Waypoint(float latitude, float longitude, float altitude, string waypointName, double waypointScale) //really, this should become the waypointmarker, and be a class that contains both a dataset (lat/long coords) and a togglable model { this.latitude = latitude; this.longitude = longitude; this.altitude = altitude; + this.waypointName = waypointName; + this.waypointScale = waypointScale; } } + */ + /// + /// Building coursebuilder tools will need: + /// A GUI, to spawn in new gates, move them, name course/points, and save data to a config node + /// A save utility class. Save Node will need: + /// CourseName string + /// WorldIndex int + /// list of WPs + /// >>each WP needs to hold a tuple - WP name string, Lat/Long/Alt Vector3d, WPScale double + /// >> these could be stored separately as a string, vector3d, double + /// + + + private List waypoints; - private List pilots; - public static List activePilots; + private List pilots; + public static List activePilots; public static List Ghosts = new List(); public static string ModelPath = "BDArmory/Models/WayPoint/model"; @@ -40,10 +62,12 @@ public WaypointFollowingStrategy(List waypoints) this.waypoints = waypoints; } + float liftMultiplier = 0; + public IEnumerator Execute(BDAScoreClient client, BDAScoreService service) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.WaypointFollowingStrategy]: Started"); - pilots = LoadedVesselSwitcher.Instance.WeaponManagers.SelectMany(tm => tm.Value).Select(wm => wm.vessel).Where(v => v != null && v.loaded).Select(v => VesselModuleRegistry.GetBDModulePilotAI(v)).Where(p => p != null).ToList(); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log("[BDArmory.WaypointFollowingStrategy]: Started"); + pilots = LoadedVesselSwitcher.Instance.WeaponManagers.SelectMany(tm => tm.Value).Select(wm => wm.vessel).Where(v => v != null && v.loaded).Select(v => VesselModuleRegistry.GetModule(v)).Where(p => p != null).ToList(); if (pilots.Count > 1) //running multiple craft through the waypoints at the same time LoadedVesselSwitcher.Instance.MassTeamSwitch(true); else //increment team each heat @@ -54,17 +78,28 @@ public IEnumerator Execute(BDAScoreClient client, BDAScoreService service) PrepareCompetition(); // Configure the pilots' waypoints. - var mappedWaypoints = waypoints.Select(e => new Vector3(e.latitude, e.longitude, e.altitude)).ToList(); + var mappedWaypoints = BDArmorySettings.WAYPOINTS_ALTITUDE == 0 ? waypoints.Select(e => e.location).ToList() : waypoints.Select(wp => new Vector3(wp.location.x, wp.location.y, BDArmorySettings.WAYPOINTS_ALTITUDE)).ToList(); BDACompetitionMode.Instance.competitionStatus.Add($"Starting waypoints competition {BDACompetitionMode.Instance.CompetitionID}."); - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log(string.Format("[BDArmory.WaypointFollowingStrategy]: Setting {0} waypoints", mappedWaypoints.Count)); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log(string.Format("[BDArmory.WaypointFollowingStrategy]: Setting {0} waypoints", mappedWaypoints.Count)); foreach (var pilot in pilots) { pilot.SetWaypoints(mappedWaypoints); + foreach (var kerbal in VesselModuleRegistry.GetKerbalEVAs(pilot.vessel)) + { + if (kerbal == null) continue; + // Remove drag from EVA kerbals on seats. + kerbal.part.dragModel = Part.DragModel.SPHERICAL; // Use the spherical drag model for which the min/max drag values work properly. + kerbal.part.ShieldedFromAirstream = true; + } } + + if (BDArmorySettings.WAYPOINTS_INFINITE_FUEL_AT_START) + { foreach (var pilot in pilots) pilot.MaintainFuelLevelsUntilWaypoint(); } + // Wait for the pilots to complete the course. var startedAt = Planetarium.GetUniversalTime(); - yield return new WaitWhile(() => pilots.Any(pilot => pilot != null && pilot.weaponManager != null && pilot.IsFlyingWaypoints && !(pilot.vessel.Landed || pilot.vessel.Splashed))); + yield return new WaitWhile(() => BDACompetitionMode.Instance.competitionIsActive && pilots.Any(pilot => pilot != null && pilot.weaponManager != null && pilot.IsRunningWaypoints && !(pilot.vessel.Landed || pilot.vessel.Splashed))); var endedAt = Planetarium.GetUniversalTime(); BDACompetitionMode.Instance.competitionStatus.Add("Waypoints competition finished. Scores:"); @@ -82,6 +117,7 @@ public IEnumerator Execute(BDAScoreClient client, BDAScoreService service) displayName += " (" + BDArmorySettings.HOS_BADGE + ")"; } BDACompetitionMode.Instance.competitionStatus.Add($" - {displayName}: Time: {elapsedTime:F1}s, Waypoints reached: {waypointCount}, Deviation: {deviation}"); + Debug.Log(string.Format("[BDArmory.WaypointFollowingStrategy]: Finished {0}, elapsed={1:0.00}, count={2}, deviation={3:0.00}", player, elapsedTime, waypointCount, deviation)); } @@ -97,9 +133,16 @@ void PrepareCompetition() BDACompetitionMode.Instance.Scores.ConfigurePlayers(pilots.Select(p => p.vessel).ToList()); if (BDArmorySettings.AUTO_ENABLE_VESSEL_SWITCHING) LoadedVesselSwitcher.Instance.EnableAutoVesselSwitching(true); + if (KerbalSafetyManager.Instance.safetyLevel != KerbalSafetyLevel.Off) + KerbalSafetyManager.Instance.CheckAllVesselsForKerbals(); if (BDArmorySettings.TIME_OVERRIDE && BDArmorySettings.TIME_SCALE != 0) { Time.timeScale = BDArmorySettings.TIME_SCALE; } Debug.Log("[BDArmory.BDACompetitionMode:" + BDACompetitionMode.Instance.CompetitionID.ToString() + "]: Starting Competition"); + if (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 55) + { + liftMultiplier = PhysicsGlobals.LiftMultiplier; + PhysicsGlobals.LiftMultiplier = 0.1f; + } if (BDArmorySettings.WAYPOINTS_VISUALIZE) { Vector3 previousLocation = FlightGlobals.ActiveVessel.transform.position; @@ -109,31 +152,37 @@ void PrepareCompetition() ModelPath = "BDArmory/Models/WayPoint/" + VesselSpawnerWindow.Instance.SelectedModel; for (int i = 0; i < waypoints.Count; i++) { - float terrainAltitude = (float)FlightGlobals.currentMainBody.TerrainAltitude(waypoints[i].latitude, waypoints[i].longitude); - Vector3d WorldCoords = VectorUtils.GetWorldSurfacePostion(new Vector3(waypoints[i].latitude, waypoints[i].longitude, waypoints[i].altitude + terrainAltitude), FlightGlobals.currentMainBody); + float terrainAltitude = (float)FlightGlobals.currentMainBody.TerrainAltitude(waypoints[i].location.x, waypoints[i].location.y); + Vector3d WorldCoords = VectorUtils.GetWorldSurfacePostion(new Vector3(waypoints[i].location.x, waypoints[i].location.y, (BDArmorySettings.WAYPOINTS_ALTITUDE == 0 ? waypoints[i].location.z : BDArmorySettings.WAYPOINTS_ALTITUDE) + terrainAltitude), FlightGlobals.currentMainBody); //FlightGlobals.currentMainBody.GetLatLonAlt(new Vector3(waypoints[i].latitude, waypoints[i].longitude, waypoints[i].altitude), out WorldCoords.x, out WorldCoords.y, out WorldCoords.z); var direction = (WorldCoords - previousLocation).normalized; - WayPointMarker.CreateWaypoint(WorldCoords, direction, ModelPath, BDArmorySettings.WAYPOINTS_SCALE); + //WayPointMarker.CreateWaypoint(WorldCoords, direction, ModelPath, BDArmorySettings.WAYPOINTS_SCALE); + WayPointMarker.CreateWaypoint(WorldCoords, direction, ModelPath, BDArmorySettings.WAYPOINTS_SCALE > 0 ? BDArmorySettings.WAYPOINTS_SCALE : waypoints[i].scale); + previousLocation = WorldCoords; - var location = string.Format("({0:##.###}, {1:##.###}, {2:####}", waypoints[i].latitude, waypoints[i].longitude, waypoints[i].altitude); - Debug.Log("[BDArmory.Waypoints]: Creating waypoint marker at " + " " + location); + var location = string.Format("({0:##.###}, {1:##.###}, {2:####}", waypoints[i].location.x, waypoints[i].location.y, waypoints[i].location.z); + Debug.Log("[BDArmory.Waypoints]: Creating waypoint marker at " + " " + location + " World: " + FlightGlobals.currentMainBody.flightGlobalsIndex + " scale: " + (BDArmorySettings.WAYPOINTS_SCALE > 0 ? BDArmorySettings.WAYPOINTS_SCALE : waypoints[i].scale)); } } - if (BDArmorySettings.WAYPOINTS_MODE || (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 50)) + if (BDArmorySettings.WAYPOINTS_MODE || (BDArmorySettings.RUNWAY_PROJECT && (BDArmorySettings.RUNWAY_PROJECT_ROUND == 50 || BDArmorySettings.RUNWAY_PROJECT_ROUND == 55))) { - float terrainAltitude = (float)FlightGlobals.currentMainBody.TerrainAltitude(waypoints[0].latitude, waypoints[0].longitude); - Vector3d WorldCoords = VectorUtils.GetWorldSurfacePostion(new Vector3(waypoints[0].latitude, waypoints[0].longitude, waypoints[0].altitude + terrainAltitude), FlightGlobals.currentMainBody); + float terrainAltitude = (float)FlightGlobals.currentMainBody.TerrainAltitude(waypoints[0].location.x, waypoints[0].location.y); + Vector3d WorldCoords = VectorUtils.GetWorldSurfacePostion(new Vector3(waypoints[0].location.x, waypoints[0].location.y, waypoints[0].location.z + terrainAltitude), FlightGlobals.currentMainBody); foreach (var pilot in pilots) { - if (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 50) // S4R10 alt limiter + if (BDArmorySettings.RUNWAY_PROJECT && (BDArmorySettings.RUNWAY_PROJECT_ROUND == 50 || BDArmorySettings.RUNWAY_PROJECT_ROUND == 55)) // S4R10 alt limiter { - if (pilot == null) continue; - // Max Altitude must be 100. - pilot.maxAltitudeToggle = true; - pilot.maxAltitude = 100f; - pilot.minAltitude = Mathf.Min(pilot.minAltitude, 50f); // Waypoints are at 50, so anything higher than this is going to trigger gain alt all the time. - pilot.defaultAltitude = Mathf.Min(pilot.defaultAltitude, 100f); + var pilotAI = pilot as BDModulePilotAI; + if (pilotAI != null) + { + // Max Altitude must be 100. + pilotAI.maxAltitudeToggle = true; + pilotAI.maxAltitude = Mathf.Min(pilotAI.maxAltitude, 100f); + pilotAI.minAltitude = Mathf.Min(pilotAI.minAltitude, 50f); // Waypoints are at 50, so anything higher than this is going to trigger gain alt all the time. + pilotAI.defaultAltitude = Mathf.Clamp(pilotAI.defaultAltitude, pilotAI.minAltitude, pilotAI.maxAltitude); + if (BDArmorySettings.RUNWAY_PROJECT_ROUND == 55) pilotAI.ImmelmannTurnAngle = 0; // Set the Immelmann turn angle to 0 since most of these craft dont't pitch well. + } } /* if (pilots.Count > 1) //running multiple craft through the waypoints at the same time @@ -189,6 +238,16 @@ void PrepareCompetition() var HPT = part.Current.FindModuleImplementing(); HPT.defenseMutator = (float)(1 / BDArmorySettings.HOS_DMG); } + if (BDArmorySettings.HOS_SAS) + { + if (part.Current.GetComponent() != null) + { + ModuleReactionWheel SAS; //could have torque reduced per hit + SAS = part.Current.GetComponent(); + //if (part.Current.CrewCapacity == 0) + part.Current.RemoveModule(SAS); //don't strip reaction wheels from cockpits, as those are allowed + } + } if (BDArmorySettings.HOS_THRUST != 100) { using (var engine = VesselModuleRegistry.GetModuleEngines(pilot.vessel).GetEnumerator()) @@ -200,6 +259,26 @@ void PrepareCompetition() } } } + if (BDArmorySettings.RUNWAY_PROJECT) + { + float torqueQuantity = 0; + using (List.Enumerator part = pilot.vessel.Parts.GetEnumerator()) + while (part.MoveNext()) + if (part.Current.GetComponent() != null) + { + ModuleReactionWheel SAS; + SAS = part.Current.GetComponent(); + if (part.Current.CrewCapacity == 0) + { + torqueQuantity += ((SAS.PitchTorque + SAS.RollTorque + SAS.YawTorque) / 3) * (SAS.authorityLimiter / 100); + if (torqueQuantity > BDArmorySettings.MAX_SAS_TORQUE) + { + float excessTorque = torqueQuantity - BDArmorySettings.MAX_SAS_TORQUE; + SAS.authorityLimiter = 100 - Mathf.Clamp(((excessTorque / ((SAS.PitchTorque + SAS.RollTorque + SAS.YawTorque) / 3)) * 100), 0, 100); + } + } + } + } } } } @@ -207,6 +286,11 @@ void PrepareCompetition() public void CleanUp() { if (BDACompetitionMode.Instance.competitionIsActive) BDACompetitionMode.Instance.StopCompetition(); // Competition is done, so stop it and do the rest of the book-keeping. + if (liftMultiplier > 0) + { + PhysicsGlobals.LiftMultiplier = liftMultiplier; + liftMultiplier = 0; + } } } @@ -243,8 +327,6 @@ public static void CreateWaypoint(Vector3 position, Vector3 direction, string Mo newWayPoint.transform.SetPositionAndRotation(position, rotation); - //newWayPoint.transform.SetPositionAndRotation(position, rotation); - //rotation based on root(partTools) transform of the model, with Z+ forward, and Y+ up newWayPoint.transform.RotateAround(position, newWayPoint.transform.up, Vector3.Angle(newWayPoint.transform.forward, direction)); //rotate model on horizontal plane towards last gate newWayPoint.transform.RotateAround(position, newWayPoint.transform.right, Vector3.Angle(newWayPoint.transform.forward, direction)); //and on vertical plane if elevation change between the two @@ -256,7 +338,7 @@ public static void CreateWaypoint(Vector3 position, Vector3 direction, string Mo } void Awake() { - transform.parent = FlightGlobals.ActiveVessel.mainBody.transform; //FIXME need to update this to grab worldindex for non-kerbin spawns for custom track building + transform.parent = FlightGlobals.ActiveVessel.mainBody.transform; } private void OnEnable() { @@ -278,7 +360,7 @@ public class WayPointTracing : MonoBehaviour { public static ObjectPool TracePool; public Vector3 Position { get; set; } - public BDModulePilotAI vessel { get; set; } + public BDGenericAIBase AI { get; set; } private List pathPoints = new List(); @@ -294,7 +376,7 @@ static void CreateObjectPool() TracePool = ObjectPool.CreateObjectPool(ghost, 120, true, true); } - public static void CreateTracer(Vector3 position, BDModulePilotAI vessel) + public static void CreateTracer(Vector3 position, BDGenericAIBase AI) { if (TracePool == null) { @@ -305,7 +387,7 @@ public static void CreateTracer(Vector3 position, BDModulePilotAI vessel) WayPointTracing NWP = newTrace.GetComponent(); NWP.Position = position; - NWP.vessel = vessel; + NWP.AI = AI; NWP.setupRenderer(); newTrace.SetActive(true); WaypointFollowingStrategy.Ghosts.Add(NWP); @@ -338,7 +420,7 @@ void setupRenderer() Debug.Log("[WayPointTracer] setting up Renderer"); Transform tf = this.transform; tracerRenderer = tf.gameObject.AddOrGetComponent(); - Color Color = BDTISetup.Instance.ColorAssignments[vessel.weaponManager.Team.Name]; //hence the incrementing teams in One-at-a-Time mode + Color Color = BDTISetup.Instance.ColorAssignments[AI.weaponManager.Team.Name]; //hence the incrementing teams in One-at-a-Time mode tracerRenderer.material = new Material(Shader.Find("KSP/Particles/Alpha Blended")); tracerRenderer.material.SetColor("_TintColor", Color); tracerRenderer.material.mainTexture = GameDatabase.Instance.GetTexture("BDArmory/Textures/laser", false); @@ -378,10 +460,10 @@ void FixedUpdate() if (HighLogic.LoadedSceneIsFlight) { if (BDArmorySetup.GameIsPaused) return; - if (vessel.vessel == null) return; - if (vessel.vessel.situation == Vessel.Situations.ORBITING || vessel.vessel.situation == Vessel.Situations.ESCAPING) return; + if (AI.vessel == null) return; + if (AI.vessel.situation == Vessel.Situations.ORBITING || AI.vessel.situation == Vessel.Situations.ESCAPING) return; - if ((!replayGhost && vessel.GetWaypointIndex() > 0) || (replayGhost && WaypointFollowingStrategy.activePilots[0].GetWaypointIndex() > 0)) + if ((!replayGhost && AI.CurrentWaypointIndex > 0) || (replayGhost && WaypointFollowingStrategy.activePilots[0].CurrentWaypointIndex > 0)) { //don't record before first WP timer += Time.fixedDeltaTime; if (timer > 1) @@ -391,7 +473,7 @@ void FixedUpdate() //Vector3d WorldCoords = VectorUtils.GetWorldSurfacePostion(wm.Current.vessel.transform.position, FlightGlobals.currentMainBody); if (!replayGhost) { - pathPoints.Add(vessel.vessel.transform.position); + pathPoints.Add(AI.vessel.transform.position); } //if (BDArmorySettings.DRAW_VESSEL_TRAILS) @@ -409,7 +491,7 @@ void FixedUpdate() //renderer was attached to a WayPointTrace class so positions would always remain consistant relative the tracer, not the ship } } - if (!replayGhost && nodes > 1) tracerRenderer.SetPosition(tracerRenderer.positionCount - 1, vessel.vessel.CoM); //have last position update real-time with vessel position + if (!replayGhost && nodes > 1) tracerRenderer.SetPosition(tracerRenderer.positionCount - 1, AI.vessel.CoM); //have last position update real-time with vessel position } else { diff --git a/BDArmory/Competition/RemoteOrchestration/BDAScoreClient.cs b/BDArmory/Competition/RemoteOrchestration/BDAScoreClient.cs index 95faafe1c..bc447c0b6 100644 --- a/BDArmory/Competition/RemoteOrchestration/BDAScoreClient.cs +++ b/BDArmory/Competition/RemoteOrchestration/BDAScoreClient.cs @@ -7,7 +7,8 @@ using System.Text.RegularExpressions; using UnityEngine; using UnityEngine.Networking; -using BDArmory.Core; + +using BDArmory.Settings; namespace BDArmory.Competition.RemoteOrchestration { @@ -358,6 +359,7 @@ private void ReceiveHeatVessels(string response) Debug.LogWarning(string.Format("[BDArmory.BDAScoreClient] Failed to parse heat vessel collection: {0}", response)); return; } + SwapCraftFiles(); foreach (VesselModel vesselModel in collection) { activeVessels.Add(vesselModel.id); @@ -480,35 +482,56 @@ private void SaveCraftFile(VesselModel vessel, byte[] bytes) .Select(e => Regex.Replace(e, pattern, "ship = " + vesselName)) .Where(e => !e.Contains("VESSELNAMING")) .ToArray(); + pattern = ".*version = (.+)"; + modifiedLines = modifiedLines + .Select(e => Regex.Replace(e, pattern, "version = 1.12.2")) + .ToArray(); File.WriteAllLines(filename, modifiedLines); Debug.Log(string.Format("[BDArmory.BDAScoreClient] Saved craft for player {0}", vesselName)); - if (vesselName.Contains(BDArmorySettings.REMOTE_ORCHESTRATION_NPC_SWAPPER)) //grab either ships or players that contain NPC identifier - { - SwapCraftFiles(vesselName); //doing this after initial load/editing to make sure nothing breaks by swapping earlier - } + //if (vesselName.Contains(BDArmorySettings.REMOTE_ORCHESTRATION_NPC_SWAPPER)) //grab either ships or players that contain NPC identifier + //{ + // SwapCraftFiles(vesselName); //doing this after initial load/editing to make sure nothing breaks by swapping earlier + //} } - public void SwapCraftFiles(string vesselname) + public void SwapCraftFiles() { - string filename = string.Format("{0}/{1}.craft", vesselPath, vesselname); - Debug.Log("[BDArmory.BDAScoreClient] Swapping existing craft in spawn directory " + vesselPath); - DirectoryInfo info = new DirectoryInfo(NPCPath); - FileInfo[] craftFiles = info.GetFiles("*.craft") - .Where(e => e.Extension == ".craft") - .ToArray(); - int i; - i = (int)UnityEngine.Random.Range(0, craftFiles.Count() - 1); + DirectoryInfo info = new DirectoryInfo(vesselStagingPath); + FileInfo[] craftFiles = info.GetFiles("*.craft"); + //.Where(e => e.Extension == ".craft").ToArray(); + if (!Directory.Exists(NPCPath)) + { + Debug.Log("[BDArmory.BDAScoreClient] Creating staging directory " + NPCPath); + Directory.CreateDirectory(NPCPath); + } + DirectoryInfo NPCinfo = new DirectoryInfo(NPCPath); + FileInfo[] NPCFiles = NPCinfo.GetFiles("*.craft"); - string NPCfilename = string.Format("{0}/{1}", NPCPath, craftFiles[i].Name); //.craft included in the craftFiles[i].name - string[] NPClines = File.ReadAllLines(NPCfilename); //kludge, probably easier to just copy the file from NPC dir to the autospawn dir - string pattern = ".*ship = (.+)"; - string[] modifiedLines = NPClines - .Select(e => Regex.Replace(e, pattern, "ship = " + vesselname)) - .Where(e => !e.Contains("VESSELNAMING")) - .ToArray(); - File.WriteAllLines(filename, modifiedLines); - Debug.Log(string.Format("[BDArmory.BDAScoreClient] Swapped craft for player {0}", vesselname)); + foreach (FileInfo file in craftFiles) + { + if (file.Name.Contains(BDArmorySettings.REMOTE_ORCHESTRATION_NPC_SWAPPER)) + { + string vesselname = file.Name; + string filename = string.Format("{0}/{1}", vesselStagingPath, vesselname); + vesselname = vesselname.Remove(vesselname.Length - 6, 6); //cull the .craft from the string + Debug.Log("[BDArmory.BDAScoreClient] Swapping existing craft " + vesselname + " in spawn directory"); + + int i; + i = (int)UnityEngine.Random.Range(0, NPCFiles.Count() - 1); + Debug.Log(string.Format("[BDArmory.BDAScoreClient] {0} craft, selected number {1}", NPCFiles.Count(), i)); + + string NPCfilename = string.Format("{0}/{1}", NPCPath, NPCFiles[i].Name); //.craft included in the craftFiles[i].name + string[] NPClines = File.ReadAllLines(NPCfilename); //kludge, probably easier to just copy the file from NPC dir to the autospawn dir + string pattern = ".*ship = (.+)"; + string[] modifiedLines = NPClines + .Select(e => Regex.Replace(e, pattern, "ship = " + vesselname)) + .Where(e => !e.Contains("VESSELNAMING")) + .ToArray(); + File.WriteAllLines(filename, modifiedLines); + Debug.Log(string.Format("[BDArmory.BDAScoreClient] Swapped craft {0} with NPC {1}", vesselname, NPCfilename)); + } + } } /// diff --git a/BDArmory/Competition/RemoteOrchestration/BDAScoreModels.cs b/BDArmory/Competition/RemoteOrchestration/BDAScoreModels.cs index 7a87b6e78..0d950b89b 100644 --- a/BDArmory/Competition/RemoteOrchestration/BDAScoreModels.cs +++ b/BDArmory/Competition/RemoteOrchestration/BDAScoreModels.cs @@ -214,6 +214,7 @@ public class RecordModel public int roc_parts_in; public double roc_dmg_out; public double roc_dmg_in; + public int ast_parts_in; public int assists; public int kills; public int deaths; diff --git a/BDArmory/Competition/RemoteOrchestration/BDAScoreService.cs b/BDArmory/Competition/RemoteOrchestration/BDAScoreService.cs index 57c2f0e61..fcda14f68 100644 --- a/BDArmory/Competition/RemoteOrchestration/BDAScoreService.cs +++ b/BDArmory/Competition/RemoteOrchestration/BDAScoreService.cs @@ -4,8 +4,8 @@ using System.ComponentModel; using System.Linq; using UnityEngine; -using BDArmory.Competition.VesselSpawning; -using BDArmory.Core; + +using BDArmory.Settings; using BDArmory.UI; namespace BDArmory.Competition.RemoteOrchestration @@ -43,6 +43,7 @@ public class BDAScoreService : MonoBehaviour public Dictionary rocketPartsIn = new Dictionary(); public Dictionary rocketDamageOut = new Dictionary(); public Dictionary rocketDamageIn = new Dictionary(); + public Dictionary asteroidPartsIn = new Dictionary(); public Dictionary waypoints = new Dictionary(); public Dictionary elapsedTime = new Dictionary(); // AUBRANIUM, I'd recommend renaming elapsedTime and deviation as waypointsElapsedTime and waypointsDeviation for clarity. Similarly for the Compute... functions. public Dictionary deviation = new Dictionary(); @@ -114,7 +115,7 @@ void Awake() void Update() { - if (syncActive && !Core.BDArmorySettings.REMOTE_LOGGING_ENABLED) + if (syncActive && !BDArmorySettings.REMOTE_LOGGING_ENABLED) { Debug.Log("[BDArmory.BDAScoreService] Cancel due to disable"); syncActive = false; @@ -193,7 +194,7 @@ public IEnumerator SynchronizeWithService(string hash) private IEnumerator CoordinateTournament(string hash) { - if( client.competition == null ) + if (client.competition == null) { Debug.Log("[BDArmory.BDAScoreService] Unexpected null competition"); status = StatusType.Invalid; @@ -268,7 +269,7 @@ private IEnumerator RunHeatCycle(string hash, HeatModel heat) yield return client.GetHeatVessels(hash, heat); // check for active vessels - if( client.activeVessels.Count == 0 ) + if (client.activeVessels.Count == 0) { Debug.Log("[BDArmory.BDAScoreService] Unexpected empty active vessel set"); yield break; @@ -279,7 +280,7 @@ private IEnumerator RunHeatCycle(string hash, HeatModel heat) yield return client.StartHeat(hash, heat); // check active heat (null means start failed) - if( client.activeHeat == null ) + if (client.activeHeat == null) { Debug.Log("[BDArmory.BDAScoreService] Unable to start heat"); yield break; @@ -290,7 +291,7 @@ private IEnumerator RunHeatCycle(string hash, HeatModel heat) // run heat using tournament coordinator var coordinator = RemoteTournamentCoordinator.BuildFromDescriptor(client.competition); - if( coordinator == null ) + if (coordinator == null) { Debug.Log("[BDArmory.BDAScoreService] Failed to build tournament coordinator"); yield break; @@ -325,7 +326,6 @@ private List BuildRecords(string hash, HeatModel heat) if (!client.playerVessels.ContainsKey(playerName)) { Debug.Log(string.Format("[BDArmory.BDAScoreService] Unmatched player {0}", playerName)); - Debug.Log("DEBUG players were " + string.Join(", ", client.players.Values)); continue; } @@ -334,7 +334,6 @@ private List BuildRecords(string hash, HeatModel heat) if (player == null) { Debug.Log(string.Format("[BDArmory.BDAScoreService] Unmatched player {0}", playerNamePart)); - Debug.Log("DEBUG players were " + string.Join(", ", client.players.Values)); continue; } @@ -343,7 +342,6 @@ private List BuildRecords(string hash, HeatModel heat) if (vessel == null) { Debug.Log(string.Format("[BDArmory.BDAScoreService] Unmatched vessel for playerId {0}", player.id)); - Debug.Log("DEBUG vessels were " + string.Join(", ", client.vessels.Values.Select(p => p.id))); continue; } @@ -369,6 +367,7 @@ private List BuildRecords(string hash, HeatModel heat) record.roc_parts_in = ComputeTotalRocketPartsIn(playerName); record.roc_dmg_out = ComputeTotalRocketDamageOut(playerName); record.roc_dmg_in = ComputeTotalRocketDamageIn(playerName); + record.ast_parts_in = ComputeTotalAsteroidPartsIn(playerName); record.wins = ComputeWins(playerName); record.kills = ComputeTotalKills(playerName); record.deaths = ComputeTotalDeaths(playerName); @@ -430,6 +429,7 @@ public void ClearScores() missilePartsOut.Clear(); rammedPartsIn.Clear(); rammedPartsOut.Clear(); + asteroidPartsIn.Clear(); } public int ComputeTotalHitsOut(string playerName) @@ -570,6 +570,13 @@ private double ComputeTotalRocketDamageIn(string playerName) return 0; } + private int ComputeTotalAsteroidPartsIn(string playerName) + { + if (asteroidPartsIn.ContainsKey(playerName)) + return asteroidPartsIn[playerName]; + return 0; + } + private int ComputeTotalKills(string playerName) { int result = 0; @@ -646,7 +653,7 @@ public float ComputeDeathTime(string playerName) public int ComputeWaypoints(string playerName) { - if( waypoints.ContainsKey(playerName) ) + if (waypoints.ContainsKey(playerName)) { return waypoints[playerName]; } @@ -658,7 +665,7 @@ public int ComputeWaypoints(string playerName) public float ComputeElapsedTime(string playerName) { - if( elapsedTime.ContainsKey(playerName) ) + if (elapsedTime.ContainsKey(playerName)) { return (float)elapsedTime[playerName]; } @@ -670,7 +677,7 @@ public float ComputeElapsedTime(string playerName) public float ComputeDeviation(string playerName) { - if( deviation.ContainsKey(playerName) ) + if (deviation.ContainsKey(playerName)) { return (float)deviation[playerName]; } @@ -682,7 +689,7 @@ public float ComputeDeviation(string playerName) public void TrackDamage(string attacker, string target, double damage) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_OTHER) { Debug.Log(string.Format("[BDArmory.BDAScoreService] TrackDamage by {0} on {1} for {2}hp", target, attacker, damage)); } @@ -708,7 +715,7 @@ public void TrackDamage(string attacker, string target, double damage) public void TrackMissileStrike(string attacker, string target) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_OTHER) { Debug.Log(string.Format("[BDArmory.BDAScoreService] TrackMissileStrike by {0} on {1}", target, attacker)); } @@ -734,7 +741,7 @@ public void TrackMissileStrike(string attacker, string target) public void TrackMissileDamage(string attacker, string target, double damage) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_OTHER) { Debug.Log(string.Format("[BDArmory.BDAScoreService] TrackMissileDamage by {0} on {1} for {2}hp", target, attacker, damage)); } @@ -760,7 +767,7 @@ public void TrackMissileDamage(string attacker, string target, double damage) public void TrackMissileParts(string attacker, string target, int count) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_OTHER) Debug.Log(string.Format("[BDArmory.BDAScoreService] TrackMissileParts by {0} on {1} for {2}parts", target, attacker, count)); double now = Planetarium.GetUniversalTime(); @@ -790,7 +797,7 @@ public void TrackMissileParts(string attacker, string target, int count) public void TrackRocketStrike(string attacker, string target) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_OTHER) { Debug.Log(string.Format("[BDArmory.BDAScoreService] TrackRocketStrike by {0} on {1}", target, attacker)); } @@ -816,7 +823,7 @@ public void TrackRocketStrike(string attacker, string target) public void TrackRocketDamage(string attacker, string target, double damage) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_OTHER) { Debug.Log(string.Format("[BDArmory.BDAScoreService] TrackRocketDamage by {0} on {1} for {2}hp", target, attacker, damage)); } @@ -842,7 +849,7 @@ public void TrackRocketDamage(string attacker, string target, double damage) public void TrackRocketParts(string attacker, string target, int count) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_OTHER) Debug.Log(string.Format("[BDArmory.BDAScoreService] TrackRocketParts by {0} on {1} for {2}parts", target, attacker, count)); double now = Planetarium.GetUniversalTime(); @@ -872,7 +879,7 @@ public void TrackRocketParts(string attacker, string target, int count) public void TrackRammedParts(string attacker, string target, int count) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_OTHER) Debug.Log(string.Format("[BDArmory.BDAScoreService] TrackRammedParts by {0} on {1} for {2}parts", target, attacker, count)); double now = Planetarium.GetUniversalTime(); @@ -900,9 +907,18 @@ public void TrackRammedParts(string attacker, string target, int count) } } + public void TrackPartsLostToAsteroids(string target, int count) + { + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.BDAScoreService]: TrackPartsLostToAsteroids by {target} for {count} parts."); + + activePlayers.Add(target); + if (asteroidPartsIn.ContainsKey(target)) asteroidPartsIn[target] += count; + else asteroidPartsIn.Add(target, count); + } + public void TrackHit(string attacker, string target, string weaponName, double hitDistance) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_OTHER) { Debug.Log(string.Format("[BDArmory.BDAScoreService] TrackHit by {0} on {1} with {2} at {3}m", target, attacker, weaponName, hitDistance)); } @@ -944,7 +960,7 @@ public void TrackHit(string attacker, string target, string weaponName, double h } if (!longestHitDistance.ContainsKey(attacker) || hitDistance > longestHitDistance[attacker]) { - Debug.Log(string.Format("[BDACompetitionMode] Tracked longest hit for {0} with {1} at {2}m", attacker, weaponName, hitDistance)); + Debug.Log(string.Format("[BDArmory.BDACompetitionMode]: Tracked longest hit for {0} with {1} at {2}m", attacker, weaponName, hitDistance)); if (longestHitDistance.ContainsKey(attacker)) { longestHitWeapon[attacker] = weaponName; @@ -1002,7 +1018,7 @@ public void ComputeAssists(string target, string killer = "", double timeLimit = */ public void TrackDeath(string target) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_OTHER) { Debug.Log(string.Format("[BDArmory.BDAScoreService] TrackDeath for {0}", target)); } @@ -1014,7 +1030,7 @@ private void IncrementDeath(string target) { if (deaths.ContainsKey(target)) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_OTHER) { Debug.Log(string.Format("[BDArmory.BDAScoreService] IncrementDeaths for {0}", target)); } @@ -1022,7 +1038,7 @@ private void IncrementDeath(string target) } else { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_OTHER) { Debug.Log(string.Format("[BDArmory.BDAScoreService] FirstDeath for {0}", target)); } @@ -1035,7 +1051,7 @@ private void IncrementDeath(string target) */ public void TrackKill(string attacker, string target) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_OTHER) { Debug.Log(string.Format("[BDArmory.BDAScoreService] TrackKill {0} by {1}", target, attacker)); } @@ -1053,7 +1069,7 @@ private void IncrementKill(string attacker, string target) { if (killsOnTarget[attacker].ContainsKey(target)) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_OTHER) { Debug.Log(string.Format("[BDArmory.BDAScoreService] IncrementKills for {0} on {1}", attacker, target)); } @@ -1061,7 +1077,7 @@ private void IncrementKill(string attacker, string target) } else { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_OTHER) { Debug.Log(string.Format("[BDArmory.BDAScoreService] Kill for {0} on {1}", attacker, target)); } @@ -1070,7 +1086,7 @@ private void IncrementKill(string attacker, string target) } else { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_OTHER) { Debug.Log(string.Format("[BDArmory.BDAScoreService] FirstKill for {0} on {1}", attacker, target)); } diff --git a/BDArmory/Competition/RemoteTournamentCoordinator.cs b/BDArmory/Competition/RemoteTournamentCoordinator.cs index 3e75f08a1..df7ae7557 100644 --- a/BDArmory/Competition/RemoteTournamentCoordinator.cs +++ b/BDArmory/Competition/RemoteTournamentCoordinator.cs @@ -2,12 +2,14 @@ using System.Collections; using System.Collections.Generic; using System.Linq; +using UnityEngine; + using BDArmory.Competition.OrchestrationStrategies; using BDArmory.Competition.RemoteOrchestration; -using BDArmory.Competition.SpawnStrategies; -using BDArmory.Competition.VesselSpawning; -using BDArmory.Core; -using UnityEngine; +using BDArmory.GameModes.Waypoints; +using BDArmory.Settings; +using BDArmory.VesselSpawning.SpawnStrategies; +using BDArmory.VesselSpawning; using static BDArmory.Competition.OrchestrationStrategies.WaypointFollowingStrategy; namespace BDArmory.Competition @@ -16,9 +18,9 @@ public class RemoteTournamentCoordinator { private SpawnStrategy spawnStrategy; private OrchestrationStrategy orchestrator; - private VesselSpawner vesselSpawner; + private VesselSpawnerBase vesselSpawner; - public RemoteTournamentCoordinator(SpawnStrategy spawner, OrchestrationStrategy orchestrator, VesselSpawner vesselSpawner) + public RemoteTournamentCoordinator(SpawnStrategy spawner, OrchestrationStrategy orchestrator, VesselSpawnerBase vesselSpawner) { this.spawnStrategy = spawner; this.orchestrator = orchestrator; @@ -33,7 +35,7 @@ public IEnumerator Execute() // first, spawn vessels yield return spawnStrategy.Spawn(vesselSpawner); - if( !spawnStrategy.DidComplete() ) + if (!spawnStrategy.DidComplete()) { Debug.Log("[BDArmory.BDAScoreService] TournamentCoordinator spawn failed"); yield break; @@ -45,12 +47,12 @@ public IEnumerator Execute() public static RemoteTournamentCoordinator BuildFromDescriptor(CompetitionModel competitionModel) { - switch(competitionModel.mode) + switch (competitionModel.mode) { case "ffa": return BuildFFA(); case "path": - return BuildShortCanyonWaypoint(); + return BuildWaypoint(); case "chase": return BuildChase(); } @@ -66,8 +68,9 @@ private static RemoteTournamentCoordinator BuildFFA() var activeVesselIds = scoreClient.activeVessels.ToList(); var craftUrls = activeVesselModels.Select(e => e.craft_url); // TODO: need coords from descriptor, or fallback to local settings - var kerbin = FlightGlobals.GetBodyByName("Kerbin"); - var bodyIndex = FlightGlobals.GetBodyIndex(kerbin); + // var kerbin = FlightGlobals.GetBodyByName("Kerbin"); + // var bodyIndex = FlightGlobals.GetBodyIndex(kerbin); + var bodyIndex = BDArmorySettings.VESSEL_SPAWN_WORLDINDEX; var latitude = BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.x; var longitude = BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.y; var altitude = BDArmorySettings.VESSEL_SPAWN_ALTITUDE; @@ -78,7 +81,7 @@ private static RemoteTournamentCoordinator BuildFFA() return new RemoteTournamentCoordinator(spawnStrategy, orchestrationStrategy, vesselSpawner); } - private static RemoteTournamentCoordinator BuildShortCanyonWaypoint() + private static RemoteTournamentCoordinator BuildWaypoint() { var scoreService = BDAScoreService.Instance; var scoreClient = scoreService.client; @@ -89,55 +92,39 @@ private static RemoteTournamentCoordinator BuildShortCanyonWaypoint() // TODO: need coords from descriptor, or fallback to local settings //var latitude = BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.x; //var longitude = BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.y; - var worldIndex = 1; - var latitude = 27.97f; - var longitude = -39.35f; + var worldIndex = WaypointCourses.CourseLocations[BDArmorySettings.WAYPOINT_COURSE_INDEX].worldIndex; + var latitude = WaypointCourses.CourseLocations[BDArmorySettings.WAYPOINT_COURSE_INDEX].spawnPoint.x; + var longitude = WaypointCourses.CourseLocations[BDArmorySettings.WAYPOINT_COURSE_INDEX].spawnPoint.y; var altitude = BDArmorySettings.VESSEL_SPAWN_ALTITUDE; var spawnRadius = BDArmorySettings.VESSEL_SPAWN_DISTANCE_TOGGLE ? BDArmorySettings.VESSEL_SPAWN_DISTANCE : BDArmorySettings.VESSEL_SPAWN_DISTANCE_FACTOR; // var spawnStrategy = new PointSpawnStrategy(craftUrl, latitude, longitude, 2*altitude, 315.0f); + Debug.Log("[BDArmory.RemoteTournamentCoordinator] Creating Spawn Strategy - WorldIndex: " + worldIndex + "; course name: " + WaypointCourses.CourseLocations[BDArmorySettings.WAYPOINT_COURSE_INDEX].name); var spawnStrategy = new SpawnConfigStrategy( - new SpawnConfig( - worldIndex, - latitude, - longitude, - altitude, + new CircularSpawnConfig( + new SpawnConfig( + worldIndex, + latitude, + longitude, + altitude, + true, + true, + 0, + null, + null, + "", + activeVesselModels.Select(m => vesselSource.GetLocalPath(m.id)).ToList() + ), spawnRadius, - BDArmorySettings.VESSEL_SPAWN_DISTANCE_TOGGLE, - BDArmorySettings.VESSEL_SPAWN_EASE_IN_SPEED, - true, - true, - 0, - null, - null, - "", - activeVesselModels.Select(m => vesselSource.GetLocalPath(m.id)).ToList() + BDArmorySettings.VESSEL_SPAWN_DISTANCE_TOGGLE ) ); - // kerbin-canyon2 - // 28.33,-39.11 - // 28.83,-38.06 - // 29.54,-38.68 - // 30.15,-38.6 - // 30.83,-38.87 - // 30.73,-39.6 - // 30.9,-40.23 - // 30.83,-41.26 - var waypoints = new List { - new Waypoint(28.33f, -39.11f, BDArmorySettings.WAYPOINTS_ALTITUDE), - new Waypoint(28.83f, -38.06f, BDArmorySettings.WAYPOINTS_ALTITUDE), - new Waypoint(29.54f, -38.68f, BDArmorySettings.WAYPOINTS_ALTITUDE), - new Waypoint(30.15f, -38.6f, BDArmorySettings.WAYPOINTS_ALTITUDE), - new Waypoint(30.83f, -38.87f, BDArmorySettings.WAYPOINTS_ALTITUDE), - new Waypoint(30.73f, -39.6f, BDArmorySettings.WAYPOINTS_ALTITUDE), - new Waypoint(30.9f, -40.23f, BDArmorySettings.WAYPOINTS_ALTITUDE), - new Waypoint(30.83f, -41.26f, BDArmorySettings.WAYPOINTS_ALTITUDE), - }; + var waypoints = WaypointCourses.CourseLocations[BDArmorySettings.WAYPOINT_COURSE_INDEX].waypoints; var orchestrationStrategy = new WaypointFollowingStrategy(waypoints); // var vesselSpawner = SingleVesselSpawning.Instance; var vesselSpawner = CircularSpawning.Instance; // The CircularSpawning spawner handles single-vessel spawning using the SpawnConfig strategy and the SingleVesselSpawning spawner is not ready yet. return new RemoteTournamentCoordinator(spawnStrategy, orchestrationStrategy, vesselSpawner); } - + /* private static RemoteTournamentCoordinator BuildLongCanyonWaypoint() { var scoreService = BDAScoreService.Instance; @@ -195,7 +182,7 @@ private static RemoteTournamentCoordinator BuildLongCanyonWaypoint() var vesselSpawner = SingleVesselSpawning.Instance; return new RemoteTournamentCoordinator(spawnStrategy, orchestrationStrategy, vesselSpawner); } - + */ private static RemoteTournamentCoordinator BuildGauntletCanyonWaypoint() { var scoreService = BDAScoreService.Instance; @@ -243,7 +230,7 @@ private static RemoteTournamentCoordinator BuildGauntletCanyonWaypoint() List strategies = new List(); - if( npcCraftUrl != null ) + if (npcCraftUrl != null) { // turret locations (all spawned at 0m) // 29.861150,-38.608205,0 @@ -278,16 +265,7 @@ private static RemoteTournamentCoordinator BuildGauntletCanyonWaypoint() strategies.Add(playerStrategy); - var waypoints = new List { - new Waypoint(28.33f, -39.11f, altitude), - new Waypoint(28.83f, -38.06f, altitude), - new Waypoint(29.54f, -38.68f, altitude), - new Waypoint(30.15f, -38.6f, altitude), - new Waypoint(30.83f, -38.87f, altitude), - new Waypoint(30.73f, -39.6f, altitude), - new Waypoint(30.9f, -40.23f, altitude), - new Waypoint(30.83f, -41.26f, altitude), - }; + var waypoints = WaypointCourses.CourseLocations[0].waypoints; //Canyon Waypoint course var orchestrationStrategy = new WaypointFollowingStrategy(waypoints); var listStrategy = new ListSpawnStrategy(strategies); var vesselSpawner = SingleVesselSpawning.Instance; diff --git a/BDArmory/Competition/Scoring.cs b/BDArmory/Competition/Scoring.cs new file mode 100644 index 000000000..ab9bf9fad --- /dev/null +++ b/BDArmory/Competition/Scoring.cs @@ -0,0 +1,991 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System; +using UnityEngine; + +using BDArmory.Competition.RemoteOrchestration; +using BDArmory.Control; +using BDArmory.GameModes.Waypoints; +using BDArmory.Settings; +using BDArmory.Utils; +using BDArmory.VesselSpawning; + +namespace BDArmory.Competition +{ + public enum DamageFrom { None, Guns, Rockets, Missiles, Ramming, Incompetence, Asteroids }; + public enum AliveState { Alive, CleanKill, HeadShot, KillSteal, AssistedKill, Dead }; + public enum GMKillReason { None, GM, OutOfAmmo, BigRedButton, LandedTooLong, Asteroids }; + + public class CompetitionScores + { + #region Public fields + public Dictionary ScoreData = new Dictionary(); + public Dictionary.KeyCollection Players => ScoreData.Keys; // Convenience variable + public int deathCount = 0; + public List deathOrder = new List(); // The names of dead players ordered by their death. + public string currentlyIT = ""; + #endregion + + #region Helper functions for registering hits, etc. + /// + /// Configure the scoring structure (wipes a previous one). + /// If a piñata is involved, include it here too. + /// + /// List of vessels involved in the competition. + public void ConfigurePlayers(List vessels) + { + if (BDArmorySettings.DEBUG_OTHER) { foreach (var vessel in vessels) { Debug.Log("[BDArmory.BDACompetitionMode.Scores]: Adding Score Tracker For " + vessel.vesselName); } } + ScoreData = vessels.ToDictionary(v => v.vesselName, v => new ScoringData()); + foreach (var vessel in vessels) + { + ScoreData[vessel.vesselName].team = VesselModuleRegistry.GetMissileFire(vessel, true).Team.Name; + } + deathCount = 0; + deathOrder.Clear(); + currentlyIT = ""; + } + /// + /// Add a competitor after the competition has started. + /// + /// + public bool AddPlayer(Vessel vessel) + { + if (ScoreData.ContainsKey(vessel.vesselName)) return false; // They're already there. + if (BDACompetitionMode.Instance.IsValidVessel(vessel) != BDACompetitionMode.InvalidVesselReason.None) return false; // Invalid vessel. + ScoreData[vessel.vesselName] = new ScoringData(); + ScoreData[vessel.vesselName].team = VesselModuleRegistry.GetMissileFire(vessel, true).Team.Name; + ScoreData[vessel.vesselName].lastFiredTime = Planetarium.GetUniversalTime(); + ScoreData[vessel.vesselName].previousPartCount = vessel.parts.Count(); + BDACompetitionMode.Instance.AddPlayerToRammingInformation(vessel); + return true; + } + /// + /// Remove a player from the competition. + /// + /// + /// + public bool RemovePlayer(string player) + { + if (!Players.Contains(player)) return false; + ScoreData.Remove(player); + BDACompetitionMode.Instance.RemovePlayerFromRammingInformation(player); + return true; + } + /// + /// Register a shot fired. + /// + /// The shooting vessel + /// true if successfully registered, false otherwise + public bool RegisterShot(string shooter) + { + if (!BDACompetitionMode.Instance.competitionIsActive) return false; + if (shooter == null || !ScoreData.ContainsKey(shooter)) return false; + if (ScoreData[shooter].aliveState != AliveState.Alive) return false; // Ignore shots fired after the vessel is dead. + ++ScoreData[shooter].shotsFired; + if (BDArmorySettings.RUNWAY_PROJECT) + { + if (BDArmorySettings.RUNWAY_PROJECT_ROUND == 41 && !BDACompetitionMode.Instance.s4r1FiringRateUpdatedFromShotThisFrame) + { + BDArmorySettings.FIRE_RATE_OVERRIDE += Mathf.Round(VectorUtils.Gaussian() * BDArmorySettings.FIRE_RATE_OVERRIDE_SPREAD + (BDArmorySettings.FIRE_RATE_OVERRIDE_CENTER - BDArmorySettings.FIRE_RATE_OVERRIDE) * BDArmorySettings.FIRE_RATE_OVERRIDE_BIAS * BDArmorySettings.FIRE_RATE_OVERRIDE_BIAS); + BDArmorySettings.FIRE_RATE_OVERRIDE = Mathf.Max(BDArmorySettings.FIRE_RATE_OVERRIDE, 10f); + BDACompetitionMode.Instance.s4r1FiringRateUpdatedFromShotThisFrame = true; + } + } + return true; + } + /// + /// Register a bullet hit. + /// + /// The attacking vessel + /// The victim vessel + /// The name of the weapon that fired the projectile + /// The distance travelled by the projectile + /// true if successfully registered, false otherwise + public bool RegisterBulletHit(string attacker, string victim, string weaponName = "", double distanceTraveled = 0) + { + if (!BDACompetitionMode.Instance.competitionIsActive) return false; + if (attacker == null || victim == null || attacker == victim || !ScoreData.ContainsKey(attacker) || !ScoreData.ContainsKey(victim)) return false; + if (ScoreData[victim].aliveState != AliveState.Alive) return false; // Ignore hits after the victim is dead. + + if (BDArmorySettings.DEBUG_OTHER) + Debug.Log($"[BDArmory.BDACompetitionMode.Scores]: {attacker} scored a hit against {victim} with {weaponName} from a distance of {distanceTraveled}m."); + + var now = Planetarium.GetUniversalTime(); + + // Attacker stats. + ++ScoreData[attacker].hits; + if (victim == BDArmorySettings.PINATA_NAME) ++ScoreData[attacker].PinataHits; //not registering hits? Try switching to victim.Contains(BDArmorySettings.PINATA_NAME)? + // Victim stats. + if (ScoreData[victim].lastPersonWhoDamagedMe != attacker) + { + ScoreData[victim].previousLastDamageTime = ScoreData[victim].lastDamageTime; + ScoreData[victim].previousPersonWhoDamagedMe = ScoreData[victim].lastPersonWhoDamagedMe; + } + if (ScoreData[victim].hitCounts.ContainsKey(attacker)) { ++ScoreData[victim].hitCounts[attacker]; } + else { ScoreData[victim].hitCounts[attacker] = 1; } + ScoreData[victim].lastDamageTime = now; + ScoreData[victim].lastDamageWasFrom = DamageFrom.Guns; + ScoreData[victim].lastPersonWhoDamagedMe = attacker; + ScoreData[victim].everyoneWhoDamagedMe.Add(attacker); + ScoreData[victim].damageTypesTaken.Add(DamageFrom.Guns); + + if (BDArmorySettings.REMOTE_LOGGING_ENABLED) + { BDAScoreService.Instance.TrackHit(attacker, victim, weaponName, distanceTraveled); } + + if (BDArmorySettings.TAG_MODE && !string.IsNullOrEmpty(weaponName)) // Empty weapon name indicates fire or other effect that doesn't count for tag mode. + { + if (ScoreData[victim].tagIsIt || string.IsNullOrEmpty(currentlyIT)) + { + if (ScoreData[victim].tagIsIt) + { + UpdateITTimeAndScore(); // Register time the victim spent as IT. + } + RegisterIsIT(attacker); // Register the attacker as now being IT. + } + } + + if (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 41 && !BDACompetitionMode.Instance.s4r1FiringRateUpdatedFromHitThisFrame) + { + BDArmorySettings.FIRE_RATE_OVERRIDE = Mathf.Round(Mathf.Min(BDArmorySettings.FIRE_RATE_OVERRIDE * BDArmorySettings.FIRE_RATE_OVERRIDE_HIT_MULTIPLIER, 1200f)); + BDACompetitionMode.Instance.s4r1FiringRateUpdatedFromHitThisFrame = true; + } + + return true; + } + /// + /// Register damage from bullets. + /// + /// Attacking vessel + /// Victim vessel + /// Amount of damage + /// true if successfully registered, false otherwise + public bool RegisterBulletDamage(string attacker, string victim, float damage) + { + if (!BDACompetitionMode.Instance.competitionIsActive) return false; + if (damage <= 0 || attacker == null || victim == null || attacker == victim || !ScoreData.ContainsKey(attacker) || !ScoreData.ContainsKey(victim)) return false; + if (ScoreData[victim].aliveState != AliveState.Alive) return false; // Ignore damage after the victim is dead. + if (float.IsNaN(damage)) + { + Debug.LogError($"DEBUG {attacker} did NaN damage to {victim}!"); + return false; + } + + if (BDArmorySettings.DEBUG_OTHER) + Debug.Log($"[BDArmory.BDACompetitionMode.Scores]: {attacker} did {damage} damage to {victim} with a gun."); + + if (ScoreData[victim].damageFromGuns.ContainsKey(attacker)) { ScoreData[victim].damageFromGuns[attacker] += damage; } + else { ScoreData[victim].damageFromGuns[attacker] = damage; } + + if (BDArmorySettings.REMOTE_LOGGING_ENABLED) + { BDAScoreService.Instance.TrackDamage(attacker, victim, damage); } + return true; + } + /// + /// Register a rocket fired. + /// + /// The shooting vessel + /// true if successfully registered, false otherwise + public bool RegisterRocketFired(string shooter) + { + if (!BDACompetitionMode.Instance.competitionIsActive) return false; + if (shooter == null || !ScoreData.ContainsKey(shooter)) return false; + if (ScoreData[shooter].aliveState != AliveState.Alive) return false; // Ignore shots fired after the vessel is dead. + ++ScoreData[shooter].rocketsFired; + return true; + } + /// + /// Register individual rocket strikes. + /// Note: this includes both kinetic and explosive strikes, so a single rocket may count for two strikes. + /// + /// + /// + /// + public bool RegisterRocketStrike(string attacker, string victim) + { + if (!BDACompetitionMode.Instance.competitionIsActive) return false; + if (attacker == null || victim == null || attacker == victim || !ScoreData.ContainsKey(attacker) || !ScoreData.ContainsKey(victim)) return false; + if (ScoreData[victim].aliveState != AliveState.Alive) return false; // Ignore hits after the victim is dead. + + if (BDArmorySettings.DEBUG_OTHER) + Debug.Log($"[BDArmory.BDACompetitionMode.Scores]: {attacker} scored a rocket strike against {victim}."); + + ++ScoreData[attacker].rocketStrikes; + if (ScoreData[victim].rocketStrikeCounts.ContainsKey(attacker)) { ++ScoreData[victim].rocketStrikeCounts[attacker]; } + else { ScoreData[victim].rocketStrikeCounts[attacker] = 1; } + + if (BDArmorySettings.REMOTE_LOGGING_ENABLED) + { BDAScoreService.Instance.TrackRocketStrike(attacker, victim); } + return true; + } + /// + /// Register the number of parts on the victim that were damaged by the attacker's rocket. + /// + /// + /// + /// + /// + public bool RegisterRocketHit(string attacker, string victim, int partsHit = 1) + { + if (!BDACompetitionMode.Instance.competitionIsActive) return false; + if (partsHit <= 0 || attacker == null || victim == null || attacker == victim || !ScoreData.ContainsKey(attacker) || !ScoreData.ContainsKey(victim)) return false; + if (ScoreData[victim].aliveState != AliveState.Alive) return false; // Ignore hits after the victim is dead. + + if (BDArmorySettings.DEBUG_OTHER) + Debug.Log($"[BDArmory.BDACompetitionMode.Scores]: {attacker} damaged {partsHit} parts on {victim} with a rocket."); + + var now = Planetarium.GetUniversalTime(); + + // Attacker stats. + ScoreData[attacker].totalDamagedPartsDueToRockets += partsHit; + + if (victim == BDArmorySettings.PINATA_NAME) ++ScoreData[attacker].PinataHits; + // Victim stats. + if (ScoreData[victim].lastPersonWhoDamagedMe != attacker) + { + ScoreData[victim].previousLastDamageTime = ScoreData[victim].lastDamageTime; + ScoreData[victim].previousPersonWhoDamagedMe = ScoreData[victim].lastPersonWhoDamagedMe; + } + if (ScoreData[victim].rocketPartDamageCounts.ContainsKey(attacker)) { ScoreData[victim].rocketPartDamageCounts[attacker] += partsHit; } + else { ScoreData[victim].rocketPartDamageCounts[attacker] = partsHit; } + ScoreData[victim].lastDamageTime = now; + ScoreData[victim].lastDamageWasFrom = DamageFrom.Rockets; + ScoreData[victim].lastPersonWhoDamagedMe = attacker; + ScoreData[victim].everyoneWhoDamagedMe.Add(attacker); + ScoreData[victim].damageTypesTaken.Add(DamageFrom.Rockets); + + if (BDArmorySettings.REMOTE_LOGGING_ENABLED) + { BDAScoreService.Instance.TrackRocketParts(attacker, victim, partsHit); } + return true; + } + /// + /// Register damage from rocket strikes. + /// + /// + /// + /// + /// + public bool RegisterRocketDamage(string attacker, string victim, float damage) + { + if (!BDACompetitionMode.Instance.competitionIsActive) return false; + if (damage <= 0 || attacker == null || victim == null || attacker == victim || !ScoreData.ContainsKey(attacker) || !ScoreData.ContainsKey(victim)) return false; + if (ScoreData[victim].aliveState != AliveState.Alive) return false; // Ignore damage after the victim is dead. + + if (BDArmorySettings.DEBUG_OTHER) + Debug.Log($"[BDArmory.BDACompetitionMode.Scores]: {attacker} did {damage} damage to {victim} with a rocket."); + + if (ScoreData[victim].damageFromRockets.ContainsKey(attacker)) { ScoreData[victim].damageFromRockets[attacker] += damage; } + else { ScoreData[victim].damageFromRockets[attacker] = damage; } + + if (BDArmorySettings.REMOTE_LOGGING_ENABLED) + { BDAScoreService.Instance.TrackRocketDamage(attacker, victim, damage); } + return true; + } + /// + /// Register damage from Battle Damage. + /// + /// + /// + /// + /// + public bool RegisterBattleDamage(string attacker, Vessel victimVessel, float damage) + { + if (!BDACompetitionMode.Instance.competitionIsActive) return false; + if (victimVessel == null) return false; + var victim = victimVessel.vesselName; + if (damage <= 0 || attacker == null || victim == null || !ScoreData.ContainsKey(attacker) || !ScoreData.ContainsKey(victim)) return false; // Note: we allow attacker=victim here to track self damage. + if (ScoreData[victim].aliveState != AliveState.Alive) return false; // Ignore damage after the victim is dead. + if (VesselModuleRegistry.GetModuleCount(victimVessel) == 0) return false; // The victim is dead, but hasn't been registered as such yet. We want to check this here as it's common for BD to occur as the vessel is killed. + + if (ScoreData[victim].battleDamageFrom.ContainsKey(attacker)) { ScoreData[victim].battleDamageFrom[attacker] += damage; } + else { ScoreData[victim].battleDamageFrom[attacker] = damage; } + + return true; + } + /// + /// Register parts lost due to ram. + /// + /// + /// + /// time the ram occured, which may be before the most recently registered damage from other sources + /// + /// true if successfully registered, false otherwise + public bool RegisterRam(string attacker, string victim, double timeOfCollision, int partsLost) + { + if (!BDACompetitionMode.Instance.competitionIsActive) return false; + if (partsLost <= 0 || attacker == null || victim == null || attacker == victim || !ScoreData.ContainsKey(attacker) || !ScoreData.ContainsKey(victim)) return false; + if (ScoreData[victim].aliveState != AliveState.Alive) return false; // Ignore rams after the victim is dead. + + if (BDArmorySettings.DEBUG_OTHER) + Debug.Log($"[BDArmory.BDACompetitionMode.Scores]: {attacker} rammed {victim} at {timeOfCollision} and the victim lost {partsLost} parts."); + + // Attacker stats. + ScoreData[attacker].totalDamagedPartsDueToRamming += partsLost; + + // Victim stats. + if (ScoreData[victim].lastDamageTime < timeOfCollision && ScoreData[victim].lastPersonWhoDamagedMe != attacker) + { + ScoreData[victim].previousLastDamageTime = ScoreData[victim].lastDamageTime; + ScoreData[victim].previousPersonWhoDamagedMe = ScoreData[victim].lastPersonWhoDamagedMe; + } + else if (ScoreData[victim].previousLastDamageTime < timeOfCollision && !string.IsNullOrEmpty(ScoreData[victim].previousPersonWhoDamagedMe) && ScoreData[victim].previousPersonWhoDamagedMe != attacker) // Newer than the current previous last damage, but older than the most recent damage from someone else. + { + ScoreData[victim].previousLastDamageTime = timeOfCollision; + ScoreData[victim].previousPersonWhoDamagedMe = attacker; + } + if (ScoreData[victim].rammingPartLossCounts.ContainsKey(attacker)) { ScoreData[victim].rammingPartLossCounts[attacker] += partsLost; } + else { ScoreData[victim].rammingPartLossCounts[attacker] = partsLost; } + if (ScoreData[victim].lastDamageTime < timeOfCollision) + { + ScoreData[victim].lastDamageTime = timeOfCollision; + ScoreData[victim].lastDamageWasFrom = DamageFrom.Ramming; + ScoreData[victim].lastPersonWhoDamagedMe = attacker; + } + ScoreData[victim].everyoneWhoDamagedMe.Add(attacker); + ScoreData[victim].damageTypesTaken.Add(DamageFrom.Ramming); + + if (BDArmorySettings.REMOTE_LOGGING_ENABLED) + { BDAScoreService.Instance.TrackRammedParts(attacker, victim, partsLost); } + return true; + } + /// + /// Register individual missile strikes. + /// + /// The vessel that launched the missile. + /// The struck vessel. + /// true if successfully registered, false otherwise + public bool RegisterMissileStrike(string attacker, string victim) + { + if (!BDACompetitionMode.Instance.competitionIsActive) return false; + if (attacker == null || victim == null || attacker == victim || !ScoreData.ContainsKey(attacker) || !ScoreData.ContainsKey(victim)) return false; + if (ScoreData[victim].aliveState != AliveState.Alive) return false; // Ignore hits after the victim is dead. + + if (BDArmorySettings.DEBUG_OTHER) + Debug.Log($"[BDArmory.BDACompetitionMode.Scores]: {attacker} scored a missile strike against {victim}."); + + if (ScoreData[victim].missileHitCounts.ContainsKey(attacker)) { ++ScoreData[victim].missileHitCounts[attacker]; } + else { ScoreData[victim].missileHitCounts[attacker] = 1; } + + if (BDArmorySettings.REMOTE_LOGGING_ENABLED) + { BDAScoreService.Instance.TrackMissileStrike(attacker, victim); } + return true; + } + /// + /// Register the number of parts on the victim that were damaged by the attacker's missile. + /// + /// The vessel that launched the missile + /// The struck vessel + /// The number of parts hit (can be 1 at a time) + /// true if successfully registered, false otherwise + public bool RegisterMissileHit(string attacker, string victim, int partsHit = 1) + { + if (!BDACompetitionMode.Instance.competitionIsActive) return false; + if (partsHit <= 0 || attacker == null || victim == null || attacker == victim || !ScoreData.ContainsKey(attacker) || !ScoreData.ContainsKey(victim)) return false; + if (ScoreData[victim].aliveState != AliveState.Alive) return false; // Ignore hits after the victim is dead. + + if (BDArmorySettings.DEBUG_OTHER) + Debug.Log($"[BDArmory.BDACompetitionMode.Scores]: {attacker} damaged {partsHit} parts on {victim} with a missile."); + + var now = Planetarium.GetUniversalTime(); + + // Attacker stats. + ScoreData[attacker].totalDamagedPartsDueToMissiles += partsHit; + + // Victim stats. + if (ScoreData[victim].lastPersonWhoDamagedMe != attacker) + { + ScoreData[victim].previousLastDamageTime = ScoreData[victim].lastDamageTime; + ScoreData[victim].previousPersonWhoDamagedMe = ScoreData[victim].lastPersonWhoDamagedMe; + } + if (ScoreData[victim].missilePartDamageCounts.ContainsKey(attacker)) { ScoreData[victim].missilePartDamageCounts[attacker] += partsHit; } + else { ScoreData[victim].missilePartDamageCounts[attacker] = partsHit; } + ScoreData[victim].lastDamageTime = now; + ScoreData[victim].lastDamageWasFrom = DamageFrom.Missiles; + ScoreData[victim].lastPersonWhoDamagedMe = attacker; + ScoreData[victim].everyoneWhoDamagedMe.Add(attacker); + ScoreData[victim].damageTypesTaken.Add(DamageFrom.Missiles); + + if (BDArmorySettings.REMOTE_LOGGING_ENABLED) + { BDAScoreService.Instance.TrackMissileParts(attacker, victim, partsHit); } + return true; + } + /// + /// Register damage from missile strikes. + /// + /// The vessel that launched the missile + /// The struck vessel + /// The amount of damage done + /// true if successfully registered, false otherwise + public bool RegisterMissileDamage(string attacker, string victim, float damage) + { + if (!BDACompetitionMode.Instance.competitionIsActive) return false; + if (damage <= 0 || attacker == null || victim == null || attacker == victim || !ScoreData.ContainsKey(attacker) || !ScoreData.ContainsKey(victim)) return false; + if (ScoreData[victim].aliveState != AliveState.Alive) return false; // Ignore damage after the victim is dead. + + if (BDArmorySettings.DEBUG_OTHER) + Debug.Log($"[BDArmory.BDACompetitionMode.Scores]: {attacker} did {damage} damage to {victim} with a missile."); + + if (ScoreData[victim].damageFromMissiles.ContainsKey(attacker)) { ScoreData[victim].damageFromMissiles[attacker] += damage; } + else { ScoreData[victim].damageFromMissiles[attacker] = damage; } + + if (BDArmorySettings.REMOTE_LOGGING_ENABLED) + { BDAScoreService.Instance.TrackMissileDamage(attacker, victim, damage); } + return true; + } + /// + /// Register a vessel dying. + /// + /// + /// true if successfully registered, false otherwise + public bool RegisterDeath(string vesselName, GMKillReason gmKillReason = GMKillReason.None, double timeOfDeath = -1) + { + if (!BDACompetitionMode.Instance.competitionIsActive) return false; + if (vesselName == null || !ScoreData.ContainsKey(vesselName)) return false; + if (ScoreData[vesselName].aliveState != AliveState.Alive) return false; // They're already dead! + + var now = timeOfDeath < 0 ? Planetarium.GetUniversalTime() : timeOfDeath; + var deathTimes = ScoreData.Values.Select(s => s.deathTime).ToList(); + var fixDeathOrder = timeOfDeath > -1 && deathTimes.Count > 0 && timeOfDeath - BDACompetitionMode.Instance.competitionStartTime < deathTimes.Max(); + deathOrder.Add(vesselName); + ScoreData[vesselName].deathOrder = deathCount++; + ScoreData[vesselName].deathTime = now - BDACompetitionMode.Instance.competitionStartTime; + ScoreData[vesselName].gmKillReason = gmKillReason; + if (fixDeathOrder) // Fix the death order if needed. + { + deathOrder = ScoreData.Where(s => s.Value.deathTime > -1).OrderBy(s => s.Value.deathTime).Select(s => s.Key).ToList(); + for (int i = 0; i < deathOrder.Count; ++i) + ScoreData[deathOrder[i]].deathOrder = i; + } + + if (BDArmorySettings.DEBUG_OTHER) + Debug.Log($"[BDArmory.BDACompetitionMode.Scores]: {vesselName} died at {ScoreData[vesselName].deathTime} (position {ScoreData[vesselName].deathOrder}), GM reason: {gmKillReason}, last damage from: {ScoreData[vesselName].lastDamageWasFrom}"); + + if (BDArmorySettings.REMOTE_LOGGING_ENABLED) + { BDAScoreService.Instance.TrackDeath(vesselName); } + + if (BDArmorySettings.TAG_MODE) + { + if (ScoreData[vesselName].tagIsIt) + { + UpdateITTimeAndScore(); // Update the final IT time for the vessel. + ScoreData[vesselName].tagIsIt = false; // Register the vessel as no longer IT. + if (gmKillReason == GMKillReason.None) // If it wasn't a GM kill, set the previous vessel that hit this one as IT. + { RegisterIsIT(ScoreData[vesselName].lastPersonWhoDamagedMe); } + else + { currentlyIT = ""; } + if (string.IsNullOrEmpty(currentlyIT)) // GM kill or couldn't find a someone else to be IT. + { BDACompetitionMode.Instance.TagResetTeams(); } + } + else if (ScoreData.ContainsKey(ScoreData[vesselName].lastPersonWhoDamagedMe) && ScoreData[ScoreData[vesselName].lastPersonWhoDamagedMe].tagIsIt) // Check to see if the IT vessel killed them. + { ScoreData[ScoreData[vesselName].lastPersonWhoDamagedMe].tagKillsWhileIt++; } + } + + if (ScoreData[vesselName].lastDamageWasFrom == DamageFrom.None || (ScoreData[vesselName].damageTypesTaken.Count == 1 && ScoreData[vesselName].damageTypesTaken.Contains(DamageFrom.Asteroids))) // Died without being hit by anyone => Incompetence + { + ScoreData[vesselName].aliveState = AliveState.Dead; + if (gmKillReason == GMKillReason.None) + { ScoreData[vesselName].lastDamageWasFrom = DamageFrom.Incompetence; } + return true; + } + + if (now - ScoreData[vesselName].lastDamageTime < BDArmorySettings.SCORING_HEADSHOT && ScoreData[vesselName].gmKillReason == GMKillReason.None && ScoreData[vesselName].lastDamageWasFrom != DamageFrom.Asteroids) // Died shortly after being hit (and not by the GM or asteroids) + { + if (ScoreData[vesselName].previousLastDamageTime < 0) // No-one else hit them => Clean kill + { ScoreData[vesselName].aliveState = AliveState.CleanKill; } + else if (now - ScoreData[vesselName].previousLastDamageTime > BDArmorySettings.SCORING_KILLSTEAL) // Last hit from someone else was a while ago => Head-shot + { ScoreData[vesselName].aliveState = AliveState.HeadShot; } + else // Last hit from someone else was recent => Kill Steal + { ScoreData[vesselName].aliveState = AliveState.KillSteal; } + + if (BDArmorySettings.REMOTE_LOGGING_ENABLED) + { BDAScoreService.Instance.TrackKill(ScoreData[vesselName].lastPersonWhoDamagedMe, vesselName); } + } + else // Survived for a while after being hit or GM kill => Assist + { + ScoreData[vesselName].aliveState = AliveState.AssistedKill; + + if (BDArmorySettings.REMOTE_LOGGING_ENABLED) + { BDAScoreService.Instance.ComputeAssists(vesselName, "", now - BDACompetitionMode.Instance.competitionStartTime); } + } + + if (BDArmorySettings.VESSEL_SPAWN_DUMP_LOG_EVERY_SPAWN && ContinuousSpawning.Instance.vesselsSpawningContinuously) ContinuousSpawning.Instance.DumpContinuousSpawningScores(); + + return true; + } + /// + /// Register the number of parts lost due to crashing into an asteroid. + /// + /// The player that crashed + /// The number of parts they lost. + /// true if successfully registered, false otherwise. + public bool RegisterAsteroidCollision(string victim, int partsDestroyed) + { + if (!BDACompetitionMode.Instance.competitionIsActive) return false; + if (partsDestroyed <= 0 || victim == null || !ScoreData.ContainsKey(victim)) return false; + + var now = Planetarium.GetUniversalTime(); + + var attacker = "Asteroids"; + if (ScoreData[victim].lastPersonWhoDamagedMe != attacker) + { + ScoreData[victim].previousLastDamageTime = ScoreData[victim].lastDamageTime; + ScoreData[victim].previousPersonWhoDamagedMe = ScoreData[victim].lastPersonWhoDamagedMe; + } + ScoreData[victim].partsLostToAsteroids += partsDestroyed; + ScoreData[victim].lastDamageTime = now; + ScoreData[victim].lastDamageWasFrom = DamageFrom.Asteroids; + ScoreData[victim].lastPersonWhoDamagedMe = attacker; + ScoreData[victim].everyoneWhoDamagedMe.Add(attacker); + ScoreData[victim].damageTypesTaken.Add(DamageFrom.Asteroids); + + if (BDArmorySettings.REMOTE_LOGGING_ENABLED) + { BDAScoreService.Instance.TrackPartsLostToAsteroids(victim, partsDestroyed); } + return true; + } + + #region Tag + public bool RegisterIsIT(string vesselName) + { + if (string.IsNullOrEmpty(vesselName) || !ScoreData.ContainsKey(vesselName)) + { + currentlyIT = ""; + return false; + } + + var now = Planetarium.GetUniversalTime(); + var vessels = BDACompetitionMode.Instance.GetAllPilots().Select(pilot => pilot.vessel).Where(vessel => Players.Contains(vessel.vesselName)).ToDictionary(vessel => vessel.vesselName, vessel => vessel); // Get the vessels so we can trigger action groups on them. Also checks that the vessels are valid competitors. + if (vessels.ContainsKey(vesselName)) // Set the player as IT if they're alive. + { + currentlyIT = vesselName; + ScoreData[vesselName].tagIsIt = true; + ScoreData[vesselName].tagTimesIt++; + ScoreData[vesselName].tagLastUpdated = now; + var mf = VesselModuleRegistry.GetMissileFire(vessels[vesselName]); + mf.SetTeam(BDTeam.Get("IT")); + mf.ForceScan(); + BDACompetitionMode.Instance.competitionStatus.Add(vesselName + " is IT!"); + vessels[vesselName].ActionGroups.ToggleGroup(BDACompetitionMode.KM_dictAG[8]); // Trigger AG8 on becoming "IT" + } + else { currentlyIT = ""; } + foreach (var player in Players) // Make sure other players are not NOT IT. + { + if (player != vesselName && vessels.ContainsKey(player)) + { + if (ScoreData[player].team != "NO") + { + ScoreData[player].tagIsIt = false; + var mf = VesselModuleRegistry.GetMissileFire(vessels[player]); + mf.SetTeam(BDTeam.Get("NO")); + mf.ForceScan(); + vessels[player].ActionGroups.ToggleGroup(BDACompetitionMode.KM_dictAG[9]); // Trigger AG9 on becoming "NOT IT" + } + } + } + return true; + } + public bool UpdateITTimeAndScore() + { + if (!string.IsNullOrEmpty(currentlyIT)) + { + if (BDACompetitionMode.Instance.previousNumberCompetitive < 2 || ScoreData[currentlyIT].landedState) return false; // Don't update if there are no competitors or we're landed. + var now = Planetarium.GetUniversalTime(); + ScoreData[currentlyIT].tagTotalTime += now - ScoreData[currentlyIT].tagLastUpdated; + ScoreData[currentlyIT].tagScore += (now - ScoreData[currentlyIT].tagLastUpdated) * BDACompetitionMode.Instance.previousNumberCompetitive * (BDACompetitionMode.Instance.previousNumberCompetitive - 1) / 5; // Rewards craft accruing time with more competitors + ScoreData[currentlyIT].tagLastUpdated = now; + } + return true; + } + #endregion + + #region Waypoints + public bool RegisterWaypointReached(string vesselName, int waypointCourseIndex, int waypointIndex, int lapNumber, int lapLimit, float distance) + { + if (!BDACompetitionMode.Instance.competitionIsActive) return false; + if (vesselName == null || !ScoreData.ContainsKey(vesselName)) return false; + + ScoreData[vesselName].waypointsReached.Add(new ScoringData.WaypointReached(waypointIndex, distance, Planetarium.GetUniversalTime() - BDACompetitionMode.Instance.competitionStartTime)); + BDACompetitionMode.Instance.competitionStatus.Add($"{vesselName}: {WaypointCourses.CourseLocations[waypointCourseIndex].waypoints[waypointIndex].name} ({waypointIndex}{(lapLimit > 1 ? $", lap {lapNumber}" : "")}) reached: Time: {ScoreData[vesselName].waypointsReached.Last().timestamp - ScoreData[vesselName].waypointsReached.First().timestamp:F2}s, Deviation: {distance:F1}m"); + ScoreData[vesselName].totalWPTime = (float)(ScoreData[vesselName].waypointsReached.Last().timestamp - ScoreData[vesselName].waypointsReached.First().timestamp); + ScoreData[vesselName].totalWPDeviation += distance; + + return true; + } + #endregion + #endregion + + public void LogResults(string CompetitionID, string message = "", string tag = "") + { + var logStrings = new List(); + logStrings.Add("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: Dumping Results" + (message != "" ? " " + message : "") + " after " + (int)(Planetarium.GetUniversalTime() - BDACompetitionMode.Instance.competitionStartTime) + "s (of " + (BDArmorySettings.COMPETITION_DURATION * 60d) + "s) at " + DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss zzz")); + + // Find out who's still alive + var alive = new HashSet(); + var survivingTeams = new HashSet(); + foreach (var vessel in FlightGlobals.Vessels) + { + if (vessel == null || !vessel.loaded || vessel.packed || VesselModuleRegistry.ignoredVesselTypes.Contains(vessel.vesselType)) + continue; + var mf = VesselModuleRegistry.GetModule(vessel); + var ai = VesselModuleRegistry.GetIBDAIControl(vessel); + double HP = 0; + double WreckFactor = 0; + if (mf != null) + { + HP = (mf.currentHP / mf.totalHP) * 100; + if (ScoreData.ContainsKey(vessel.vesselName)) + { + ScoreData[vessel.vesselName].remainingHP = HP; + survivingTeams.Add(ScoreData[vessel.vesselName].team); //move this here so last man standing can claim the win, even if they later don't meet the 'survive' criteria + } + if (HP < 100) + { + WreckFactor += (100 - HP) / 100; //the less plane remaining, the greater the chance it's a wreck + } + if (ai == null) + { + WreckFactor += 0.5f; // It's brain-dead. + } + else if (vessel.LandedOrSplashed && (ai as BDModulePilotAI != null || ai as BDModuleVTOLAI != null)) + { + WreckFactor += 0.5f; // It's a plane / helicopter that's now on the ground. + } + if (vessel.verticalSpeed < -30) //falling out of the sky? Could be an intact plane diving to default alt, could be a cockpit + { + WreckFactor += 0.5f; + var AI = ai as BDModulePilotAI; + if (AI == null || vessel.radarAltitude < AI.defaultAltitude) //craft is uncontrollably diving, not returning from high alt to cruising alt + { + WreckFactor += 0.5f; + } + } + if (VesselModuleRegistry.GetModuleCount(vessel) > 0) + { + int engineOut = 0; + foreach (var engine in VesselModuleRegistry.GetModules(vessel)) + { + if (engine == null || engine.flameout || engine.finalThrust <= 0) + engineOut++; + } + WreckFactor += (engineOut / VesselModuleRegistry.GetModuleCount(vessel)) / 2; + } + else + { + WreckFactor += 0.5f; //could be a glider, could be missing engines + } + if (WreckFactor < 1.1f) // 'wrecked' requires some combination of diving, no engines, and missing parts + { + alive.Add(vessel.vesselName); + } + } + } + + // General result. (Note: uses hand-coded JSON to make parsing easier in python.) + if (survivingTeams.Count == 0) + { + logStrings.Add("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: RESULT:Mutual Annihilation"); + } + else if (survivingTeams.Count == 1) + { // Win + var winningTeam = survivingTeams.First(); + var winningTeamMembers = ScoreData.Where(s => s.Value.team == winningTeam).Select(s => s.Key); + logStrings.Add("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: RESULT:Win:{\"team\": " + $"\"{winningTeam}\", \"members\": [" + string.Join(", ", winningTeamMembers.Select(m => $"\"{m.Replace("\"", "\\\"")}\"")) + "]}"); + } + else + { // Draw + var drawTeams = survivingTeams.ToDictionary(t => t, t => ScoreData.Where(s => s.Value.team == t).Select(s => s.Key)); + logStrings.Add("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: RESULT:Draw:[" + string.Join(", ", drawTeams.Select(t => "{\"team\": " + $"\"{t.Key}\"" + ", \"members\": [" + string.Join(", ", t.Value.Select(m => $"\"{m.Replace("\"", "\\\"")}\"")) + "]}")) + "]"); + } + { // Dead teams. + var deadTeamNames = ScoreData.Where(s => !survivingTeams.Contains(s.Value.team)).Select(s => s.Value.team).ToHashSet(); + var deadTeams = deadTeamNames.ToDictionary(t => t, t => ScoreData.Where(s => s.Value.team == t).Select(s => s.Key)); + logStrings.Add("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: DEADTEAMS:[" + string.Join(", ", deadTeams.Select(t => "{\"team\": " + $"\"{t.Key}\"" + ", \"members\": [" + string.Join(", ", t.Value.Select(m => $"\"{m.Replace("\"", "\\\"")}\"")) + "]}")) + "]"); + } + + // Record ALIVE/DEAD status of each craft. + foreach (var vesselName in alive) // List ALIVE craft first + { + logStrings.Add("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: ALIVE:" + vesselName); + } + foreach (var player in Players) // Then DEAD or MIA. + { + if (!alive.Contains(player)) + { + if (ScoreData[player].deathOrder > -1) + { + logStrings.Add("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: DEAD:" + ScoreData[player].deathOrder + ":" + ScoreData[player].deathTime.ToString("0.0") + ":" + player); // DEAD: :: + } + else + { + logStrings.Add("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: MIA:" + player); + } + } + } + + // Report survivors to Remote Orchestration + if (BDArmorySettings.REMOTE_LOGGING_ENABLED) + { BDAScoreService.Instance.TrackSurvivors(Players.Where(player => ScoreData[player].deathOrder == -1).ToList()); } + + // Who shot who. + foreach (var player in Players) + if (ScoreData[player].hitCounts.Count > 0) + { + string whoShotMe = "[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: WHOSHOTWHOWITHGUNS:" + player; + foreach (var vesselName in ScoreData[player].hitCounts.Keys) + whoShotMe += ":" + ScoreData[player].hitCounts[vesselName] + ":" + vesselName; + logStrings.Add(whoShotMe); + } + + // Damage from bullets + foreach (var player in Players) + if (ScoreData[player].damageFromGuns.Count > 0) + { + string whoDamagedMeWithGuns = "[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: WHODAMAGEDWHOWITHGUNS:" + player; + foreach (var vesselName in ScoreData[player].damageFromGuns.Keys) + whoDamagedMeWithGuns += ":" + ScoreData[player].damageFromGuns[vesselName].ToString("0.0") + ":" + vesselName; + logStrings.Add(whoDamagedMeWithGuns); + } + + // Who hit who with rockets. + foreach (var player in Players) + if (ScoreData[player].rocketStrikeCounts.Count > 0) + { + string whoHitMeWithRockets = "[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: WHOHITWHOWITHROCKETS:" + player; + foreach (var vesselName in ScoreData[player].rocketStrikeCounts.Keys) + whoHitMeWithRockets += ":" + ScoreData[player].rocketStrikeCounts[vesselName] + ":" + vesselName; + logStrings.Add(whoHitMeWithRockets); + } + + // Who hit parts by who with rockets. + foreach (var player in Players) + if (ScoreData[player].rocketPartDamageCounts.Count > 0) + { + string partHitCountsFromRockets = "[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: WHOPARTSHITWHOWITHROCKETS:" + player; + foreach (var vesselName in ScoreData[player].rocketPartDamageCounts.Keys) + partHitCountsFromRockets += ":" + ScoreData[player].rocketPartDamageCounts[vesselName] + ":" + vesselName; + logStrings.Add(partHitCountsFromRockets); + } + + // Damage from rockets + foreach (var player in Players) + if (ScoreData[player].damageFromRockets.Count > 0) + { + string whoDamagedMeWithRockets = "[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: WHODAMAGEDWHOWITHROCKETS:" + player; + foreach (var vesselName in ScoreData[player].damageFromRockets.Keys) + whoDamagedMeWithRockets += ":" + ScoreData[player].damageFromRockets[vesselName].ToString("0.0") + ":" + vesselName; + logStrings.Add(whoDamagedMeWithRockets); + } + + // Who hit who with missiles. + foreach (var player in Players) + if (ScoreData[player].missileHitCounts.Count > 0) + { + string whoHitMeWithMissiles = "[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: WHOHITWHOWITHMISSILES:" + player; + foreach (var vesselName in ScoreData[player].missileHitCounts.Keys) + whoHitMeWithMissiles += ":" + ScoreData[player].missileHitCounts[vesselName] + ":" + vesselName; + logStrings.Add(whoHitMeWithMissiles); + } + + // Who hit parts by who with missiles. + foreach (var player in Players) + if (ScoreData[player].missilePartDamageCounts.Count > 0) + { + string partHitCountsFromMissiles = "[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: WHOPARTSHITWHOWITHMISSILES:" + player; + foreach (var vesselName in ScoreData[player].missilePartDamageCounts.Keys) + partHitCountsFromMissiles += ":" + ScoreData[player].missilePartDamageCounts[vesselName] + ":" + vesselName; + logStrings.Add(partHitCountsFromMissiles); + } + + // Damage from missiles + foreach (var player in Players) + if (ScoreData[player].damageFromMissiles.Count > 0) + { + string whoDamagedMeWithMissiles = "[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: WHODAMAGEDWHOWITHMISSILES:" + player; + foreach (var vesselName in ScoreData[player].damageFromMissiles.Keys) + whoDamagedMeWithMissiles += ":" + ScoreData[player].damageFromMissiles[vesselName].ToString("0.0") + ":" + vesselName; + logStrings.Add(whoDamagedMeWithMissiles); + } + + // Who rammed who. + foreach (var player in Players) + if (ScoreData[player].rammingPartLossCounts.Count > 0) + { + string whoRammedMe = "[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: WHORAMMEDWHO:" + player; + foreach (var vesselName in ScoreData[player].rammingPartLossCounts.Keys) + whoRammedMe += ":" + ScoreData[player].rammingPartLossCounts[vesselName] + ":" + vesselName; + logStrings.Add(whoRammedMe); + } + + // Battle Damage + foreach (var player in Players) + if (ScoreData[player].battleDamageFrom.Count > 0) + { + string whoDamagedMeWithBattleDamages = "[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: WHODAMAGEDWHOWITHBATTLEDAMAGE:" + player; + foreach (var vesselName in ScoreData[player].battleDamageFrom.Keys) + whoDamagedMeWithBattleDamages += ":" + ScoreData[player].battleDamageFrom[vesselName].ToString("0.0") + ":" + vesselName; + logStrings.Add(whoDamagedMeWithBattleDamages); + } + + // GM kill reasons + foreach (var player in Players) + if (ScoreData[player].gmKillReason != GMKillReason.None) + logStrings.Add($"[BDArmory.BDACompetitionMode:{CompetitionID}]: GMKILL:{player}:{ScoreData[player].gmKillReason}"); + + // Clean kills/rams/etc. + var specialKills = new HashSet { AliveState.CleanKill, AliveState.HeadShot, AliveState.KillSteal }; + foreach (var player in Players) + { + if (specialKills.Contains(ScoreData[player].aliveState) && ScoreData[player].gmKillReason == GMKillReason.None) + { + logStrings.Add("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: " + ScoreData[player].aliveState.ToString().ToUpper() + ScoreData[player].lastDamageWasFrom.ToString().ToUpper() + ":" + player + ":" + ScoreData[player].lastPersonWhoDamagedMe); + } + } + + // Asteroids + foreach (var player in Players) + if (ScoreData[player].partsLostToAsteroids > 0) + logStrings.Add($"[BDArmory.BDACompetitionMode:{CompetitionID}]: PARTSLOSTTOASTEROIDS:{player}:{ScoreData[player].partsLostToAsteroids}"); + + // remaining health + foreach (var key in Players) + { + logStrings.Add("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: HPLEFT:" + key + ":" + ScoreData[key].remainingHP); + } + + // Accuracy + foreach (var player in Players) + { + logStrings.Add("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: ACCURACY:" + player + ":" + ScoreData[player].hits + "/" + ScoreData[player].shotsFired + ":" + ScoreData[player].rocketStrikes + "/" + ScoreData[player].rocketsFired); + } + + // Time "IT" and kills while "IT" logging + if (BDArmorySettings.TAG_MODE) + { + foreach (var player in Players) + logStrings.Add("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: TAGSCORE:" + player + ":" + ScoreData[player].tagScore.ToString("0.0")); + + foreach (var player in Players) + logStrings.Add("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: TIMEIT:" + player + ":" + ScoreData[player].tagTotalTime.ToString("0.0")); + + foreach (var player in Players) + if (ScoreData[player].tagKillsWhileIt > 0) + logStrings.Add("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: KILLSWHILEIT:" + player + ":" + ScoreData[player].tagKillsWhileIt); + + foreach (var player in Players) + if (ScoreData[player].tagTimesIt > 0) + logStrings.Add("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: TIMESIT:" + player + ":" + ScoreData[player].tagTimesIt); + } + + // Waypoints + foreach (var player in Players) + { + if (ScoreData[player].waypointsReached.Count > 0) + logStrings.Add("[BDArmory.BDACompetitionMode:" + CompetitionID.ToString() + "]: WAYPOINTS:" + player + ":" + string.Join(";", ScoreData[player].waypointsReached.Select(wp => wp.waypointIndex + ":" + wp.deviation.ToString("F2") + ":" + wp.timestamp.ToString("F2")))); + } + + // Dump the log results to a file + var folder = Path.GetFullPath(Path.Combine(KSPUtil.ApplicationRootPath, "GameData", "BDArmory", "Logs")); + if (BDATournament.Instance.tournamentStatus == TournamentStatus.Running) + { + folder = Path.Combine(folder, "Tournament " + BDATournament.Instance.tournamentID, "Round " + BDATournament.Instance.currentRound); + tag = "Heat " + BDATournament.Instance.currentHeat; + } + if (!Directory.Exists(folder)) + Directory.CreateDirectory(folder); + var fileName = Path.Combine(folder, CompetitionID.ToString() + (tag != "" ? "-" + tag : "") + ".log"); + Debug.Log($"[BDArmory.BDACompetitionMode]: Dumping competition results to {fileName}"); + File.WriteAllLines(fileName, logStrings); + } + } + + public class ScoringData + { + public AliveState aliveState = AliveState.Alive; // State of the vessel. + public string team; // The vessel's team. + + #region Guns + public int hits; // Number of hits this vessel landed. + public int PinataHits; // Number of hits this vessel landed on the piñata (included in Hits). + public int shotsFired = 0; // Number of shots fired by this vessel. + public Dictionary hitCounts = new Dictionary(); // Hits taken from guns fired by other vessels. + public Dictionary damageFromGuns = new Dictionary(); // Damage taken from guns fired by other vessels. + #endregion + + #region Rockets + public int totalDamagedPartsDueToRockets = 0; // Number of other vessels' parts damaged by this vessel due to rocket strikes. + public int rocketStrikes = 0; // Number of rockets fired by the vessel that hit someone. + public int rocketsFired = 0; // Number of rockets fired by this vessel. + public Dictionary damageFromRockets = new Dictionary(); // Damage taken from rocket hits from other vessels. + public Dictionary rocketPartDamageCounts = new Dictionary(); // Number of parts damaged by rocket hits from other vessels. + public Dictionary rocketStrikeCounts = new Dictionary(); // Number of rocket strikes from other vessels. + #endregion + + #region Ramming + public int totalDamagedPartsDueToRamming = 0; // Number of other vessels' parts destroyed by this vessel due to ramming. + public Dictionary rammingPartLossCounts = new Dictionary(); // Number of parts lost due to ramming by other vessels. + #endregion + + #region Missiles + public int totalDamagedPartsDueToMissiles = 0; // Number of other vessels' parts damaged by this vessel due to missile strikes. + public Dictionary damageFromMissiles = new Dictionary(); // Damage taken from missile strikes from other vessels. + public Dictionary missilePartDamageCounts = new Dictionary(); // Number of parts damaged by missile strikes from other vessels. + public Dictionary missileHitCounts = new Dictionary(); // Number of missile strikes from other vessels. + #endregion + + #region Special + public int partsLostToAsteroids = 0; // Number of parts lost due to crashing into asteroids. + #endregion + + #region Battle Damage + public Dictionary battleDamageFrom = new Dictionary(); // Battle damage taken from others. + #endregion + + #region GM + public double lastFiredTime; // Time that this vessel last fired a gun. + public bool landedState; // Whether the vessel is landed or not. + public double lastLandedTime; // Time that this vessel was landed last. + public double landedKillTimer; // Counter tracking time this vessel is landed (for the kill timer). + public double AltitudeKillTimer; // Counter tracking time this vessel is outside GM altitude restrictions (for kill timer). + public double AverageSpeed; // Average speed of this vessel recently (for the killer GM). + public double AverageAltitude; // Average altitude of this vessel recently (for the killer GM). + public int averageCount; // Count for the averaging stats. + public GMKillReason gmKillReason = GMKillReason.None; // Reason the GM killed this vessel. + #endregion + + #region Tag + public bool tagIsIt = false; // Whether this vessel is IT or not. + public int tagKillsWhileIt = 0; // The number of kills gained while being IT. + public int tagTimesIt = 0; // The number of times this vessel was IT. + public double tagTotalTime = 0; // The total this vessel spent being IT. + public double tagScore = 0; // Abstract score for tag mode. + public double tagLastUpdated = 0; // Time the tag time was last updated. + #endregion + + #region Waypoint + public struct WaypointReached + { + public WaypointReached(int waypointIndex, float deviation, double timestamp) { this.waypointIndex = waypointIndex; this.deviation = deviation; this.timestamp = timestamp; } + public int waypointIndex; // Number of waypoints this vessel reached. + public float deviation; // Deviation from waypoint. + public double timestamp; // Timestamp of reaching waypoint. + } + public List waypointsReached = new List(); + public float totalWPDeviation = 0; // Convenience tracker for the Vessel Switcher + public float totalWPTime = 0; // Convenience tracker for the Vessel Switcher + #endregion + + #region Misc + public int previousPartCount; // Number of parts this vessel had last time we checked (for tracking when a vessel has lost parts). + public double lastLostPartTime = 0; // Time of losing last part (up to granularity of the updateTickLength). + public double remainingHP; // HP of vessel + public double lastDamageTime = -1; + public DamageFrom lastDamageWasFrom = DamageFrom.None; + public string lastPersonWhoDamagedMe = ""; + public double previousLastDamageTime = -1; + public string previousPersonWhoDamagedMe = ""; + public int deathOrder = -1; + public double deathTime = -1; + public HashSet damageTypesTaken = new HashSet(); + public HashSet everyoneWhoDamagedMe = new HashSet(); // Every other vessel that damaged this vessel. + #endregion + } +} diff --git a/BDArmory/Competition/TournamentCoordinator.cs b/BDArmory/Competition/TournamentCoordinator.cs index cc2744feb..f76d2a5d5 100644 --- a/BDArmory/Competition/TournamentCoordinator.cs +++ b/BDArmory/Competition/TournamentCoordinator.cs @@ -4,9 +4,9 @@ using UnityEngine; using BDArmory.Competition.OrchestrationStrategies; -using BDArmory.Competition.SpawnStrategies; -using BDArmory.Competition.VesselSpawning; -using BDArmory.Core; +using BDArmory.Settings; +using BDArmory.VesselSpawning.SpawnStrategies; +using BDArmory.VesselSpawning; namespace BDArmory.Competition { @@ -16,7 +16,7 @@ public class TournamentCoordinator : MonoBehaviour public static TournamentCoordinator Instance; private SpawnStrategy spawnStrategy; private OrchestrationStrategy orchestrator; - private VesselSpawner vesselSpawner; + private VesselSpawnerBase vesselSpawner; private Coroutine executing = null; private Coroutine executingForEach = null; public bool IsRunning { get; private set; } @@ -27,7 +27,7 @@ void Awake() Instance = this; } - public void Configure(SpawnStrategy spawner, OrchestrationStrategy orchestrator, VesselSpawner vesselSpawner) + public void Configure(SpawnStrategy spawner, OrchestrationStrategy orchestrator, VesselSpawnerBase vesselSpawner) { this.spawnStrategy = spawner; this.orchestrator = orchestrator; @@ -72,7 +72,7 @@ public IEnumerator Execute() IsRunning = false; } - public void RunForEach(List strategies, OrchestrationStrategy orchestrator, VesselSpawner spawner) where T : SpawnStrategy + public void RunForEach(List strategies, OrchestrationStrategy orchestrator, VesselSpawnerBase spawner) where T : SpawnStrategy { StopForEach(); executingForEach = StartCoroutine(ExecuteForEach(strategies, orchestrator, spawner)); @@ -88,7 +88,7 @@ public void StopForEach() } } - IEnumerator ExecuteForEach(List strategies, OrchestrationStrategy orchestrator, VesselSpawner spawner) where T : SpawnStrategy + IEnumerator ExecuteForEach(List strategies, OrchestrationStrategy orchestrator, VesselSpawnerBase spawner) where T : SpawnStrategy { int i = 0; foreach (var strategy in strategies) diff --git a/BDArmory/Competition/VesselSpawning/CircularSpawning.cs b/BDArmory/Competition/VesselSpawning/CircularSpawning.cs deleted file mode 100644 index 8ed9e76d9..000000000 --- a/BDArmory/Competition/VesselSpawning/CircularSpawning.cs +++ /dev/null @@ -1,913 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using UnityEngine; -using BDArmory.Core; -using BDArmory.Core.Utils; -using BDArmory.Modules; -using BDArmory.Misc; -using BDArmory.UI; - -namespace BDArmory.Competition.VesselSpawning -{ - /// - /// Spawning of a group of craft in a ring. - /// - /// This is the default spawning code for RWP competitions currently and is essentially what the CircularSpawnStrategy needs to perform before it can take over as the default. - /// - /// TODO: - /// The central block of the SpawnAllVesselsOnce function should eventually switch to using SingleVesselSpawning.Instance.SpawnVessel (plus local coroutines for the extra stuff) to do the actual spawning of the vessels once that's ready. - /// - [KSPAddon(KSPAddon.Startup.Flight, false)] - public class CircularSpawning : VesselSpawner - { - public static CircularSpawning Instance; - - void Awake() - { - if (Instance) - Destroy(Instance); - Instance = this; - } - - private int spawnedVesselCount = 0; - private string message; - - public override IEnumerator Spawn(SpawnConfig spawnConfig) => SpawnAllVesselsOnceAsCoroutine(spawnConfig); - public void CancelSpawning() - { - // Single spawn - if (vesselsSpawning) - { - vesselsSpawning = false; - message = "Vessel spawning cancelled."; - BDACompetitionMode.Instance.competitionStatus.Add(message); - Debug.Log("[BDArmory.CircularSpawning]: " + message); - } - if (spawnAllVesselsOnceCoroutine != null) - { - StopCoroutine(spawnAllVesselsOnceCoroutine); - spawnAllVesselsOnceCoroutine = null; - } - - // Continuous single spawn - if (vesselsSpawningOnceContinuously) - { - vesselsSpawningOnceContinuously = false; - message = "Continuous single spawning cancelled."; - Debug.Log("[BDArmory.CircularSpawning]: " + message); - BDACompetitionMode.Instance.competitionStatus.Add(message); - } - if (spawnAllVesselsOnceContinuouslyCoroutine != null) - { - StopCoroutine(spawnAllVesselsOnceContinuouslyCoroutine); - spawnAllVesselsOnceContinuouslyCoroutine = null; - } - } - - #region Single spawning - /// - /// Prespawn initialisation to handle camera and body changes and to ensure that only a single spawning coroutine is running. - /// Note: This currently has some specifics to the SpawnAllVesselsOnceCoroutine, so it may not be suitable for other spawning strategies yet. - /// - /// The spawn config for the new spawning. - public override void PreSpawnInitialisation(SpawnConfig spawnConfig) - { - base.PreSpawnInitialisation(spawnConfig); - - vesselsSpawning = true; // Signal that we've started the spawning vessels routine. - vesselSpawnSuccess = false; // Set our success flag to false for now. - spawnFailureReason = SpawnFailureReason.None; - if (spawnAllVesselsOnceCoroutine != null) - StopCoroutine(spawnAllVesselsOnceCoroutine); - } - - public void SpawnAllVesselsOnce(int worldIndex, double latitude, double longitude, double altitude = 0, float distance = 10f, bool absDistanceOrFactor = false, float easeInSpeed = 1f, bool killEverythingFirst = true, bool assignTeams = true, int numberOfTeams = 0, List teamCounts = null, List> teamsSpecific = null, string spawnFolder = null, List craftFiles = null) - { - SpawnAllVesselsOnce(new SpawnConfig(worldIndex, latitude, longitude, altitude, distance, absDistanceOrFactor, easeInSpeed, killEverythingFirst, assignTeams, numberOfTeams, teamCounts, teamsSpecific, spawnFolder, craftFiles)); - } - - public void SpawnAllVesselsOnce(SpawnConfig spawnConfig) - { - PreSpawnInitialisation(spawnConfig); - spawnAllVesselsOnceCoroutine = StartCoroutine(SpawnAllVesselsOnceCoroutine(spawnConfig)); - Debug.Log("[BDArmory.CircularSpawning]: Triggering vessel spawning at " + spawnConfig.latitude.ToString("G6") + ", " + spawnConfig.longitude.ToString("G6") + ", with altitude " + spawnConfig.altitude + "m."); - } - - /// - /// A coroutine version of the SpawnAllVesselsOnce function that performs the required prespawn initialisation. - /// - /// The spawn config to use. - public IEnumerator SpawnAllVesselsOnceAsCoroutine(SpawnConfig spawnConfig) - { - PreSpawnInitialisation(spawnConfig); - Debug.Log("[BDArmory.CircularSpawning]: Triggering vessel spawning at " + spawnConfig.latitude.ToString("G6") + ", " + spawnConfig.longitude.ToString("G6") + ", with altitude " + spawnConfig.altitude + "m."); - yield return SpawnAllVesselsOnceCoroutine(spawnConfig); - } - - private Coroutine spawnAllVesselsOnceCoroutine; - // Spawns all vessels in an outward facing ring and lowers them to the ground. An altitude of 5m should be suitable for most cases. - private IEnumerator SpawnAllVesselsOnceCoroutine(int worldIndex, double latitude, double longitude, double altitude, float spawnDistanceFactor, bool absDistanceOrFactor, float easeInSpeed, bool killEverythingFirst = true, bool assignTeams = true, int numberOfTeams = 0, List teamCounts = null, List> teamsSpecific = null, string folder = null, List craftFiles = null) - { - yield return SpawnAllVesselsOnceCoroutine(new SpawnConfig(worldIndex, latitude, longitude, altitude, spawnDistanceFactor, absDistanceOrFactor, easeInSpeed, killEverythingFirst, assignTeams, numberOfTeams, teamCounts, teamsSpecific, folder, craftFiles)); - } - private IEnumerator SpawnAllVesselsOnceCoroutine(SpawnConfig spawnConfig) - { - #region Initialisation and sanity checks - // Tally up the craft to spawn. - if (spawnConfig.teamsSpecific == null) - { - if (spawnConfig.numberOfTeams == 1) // Scan subfolders - { - spawnConfig.teamsSpecific = new List>(); - var teamDirs = Directory.GetDirectories(Path.Combine(KSPUtil.ApplicationRootPath, "AutoSpawn", spawnConfig.folder)); - if (teamDirs.Length == 0) // Make teams from each vessel in the spawn folder. - { - spawnConfig.numberOfTeams = -1; // Flag for treating craft files as folder names. - spawnConfig.craftFiles = Directory.GetFiles(Path.Combine(KSPUtil.ApplicationRootPath, "AutoSpawn", spawnConfig.folder), "*.craft").ToList(); - spawnConfig.teamsSpecific = spawnConfig.craftFiles.Select(f => new List { f }).ToList(); - } - else - { - var stripStartCount = Path.Combine(KSPUtil.ApplicationRootPath, "AutoSpawn").Length; - Debug.Log("[BDArmory.CircularSpawning]: Spawning teams from folders " + string.Join(", ", teamDirs.Select(d => d.Substring(stripStartCount)))); - foreach (var teamDir in teamDirs) - { - spawnConfig.teamsSpecific.Add(Directory.GetFiles(teamDir, "*.craft").ToList()); - } - spawnConfig.craftFiles = spawnConfig.teamsSpecific.SelectMany(v => v.ToList()).ToList(); - } - } - else // Just the specified folder. - { - if (spawnConfig.craftFiles == null) // Prioritise the list of craftFiles if we're given them. - spawnConfig.craftFiles = Directory.GetFiles(Path.Combine(KSPUtil.ApplicationRootPath, "AutoSpawn", spawnConfig.folder), "*.craft").ToList(); - } - } - else // Spawn the specific vessels. - { - spawnConfig.craftFiles = spawnConfig.teamsSpecific.SelectMany(v => v.ToList()).ToList(); - } - if (spawnConfig.craftFiles.Count == 0) - { - message = "Vessel spawning: found no craft files in " + Path.Combine(KSPUtil.ApplicationRootPath, "AutoSpawn", spawnConfig.folder); - Debug.Log("[BDArmory.CircularSpawning]: " + message); - BDACompetitionMode.Instance.competitionStatus.Add(message); - vesselsSpawning = false; - spawnFailureReason = SpawnFailureReason.NoCraft; - yield break; - } - bool useOriginalTeamNames = spawnConfig.assignTeams && (spawnConfig.numberOfTeams == 1 || spawnConfig.numberOfTeams == -1); // We'll be using the folders or craft filenames as team names in the originalTeams dictionary. - if (spawnConfig.teamsSpecific != null && !useOriginalTeamNames) - { - spawnConfig.teamCounts = spawnConfig.teamsSpecific.Select(tl => tl.Count).ToList(); - } - if (BDArmorySettings.VESSEL_SPAWN_RANDOM_ORDER) spawnConfig.craftFiles.Shuffle(); // Randomise the spawn order. - spawnedVesselCount = 0; // Reset our spawned vessel count. - message = "Spawning " + spawnConfig.craftFiles.Count + " vessels at an altitude of " + spawnConfig.altitude.ToString("G0") + "m" + (spawnConfig.craftFiles.Count > 8 ? ", this may take some time..." : "."); - Debug.Log("[BDArmory.CircularSpawning]: " + message); - var spawnAirborne = spawnConfig.altitude > 10; - var spawnDistance = spawnConfig.craftFiles.Count > 1 ? (spawnConfig.absDistanceOrFactor ? spawnConfig.distance : (spawnConfig.distance + spawnConfig.distance * spawnConfig.craftFiles.Count)) : 0f; // If it's a single craft, spawn it at the spawn point. - if (BDACompetitionMode.Instance) // Reset competition stuff. - { - BDACompetitionMode.Instance.competitionStatus.Add(message); - BDACompetitionMode.Instance.LogResults("due to spawning", "auto-dump-from-spawning"); // Log results first. - BDACompetitionMode.Instance.StopCompetition(); - BDACompetitionMode.Instance.ResetCompetitionStuff(); // Reset competition scores. - BDACompetitionMode.Instance.RemoveDebrisNow(); // Remove debris and space junk. - } - yield return waitForFixedUpdate; - #endregion - - #region Pre-spawning - if (spawnConfig.killEverythingFirst) - { - var vesselsToKill = FlightGlobals.Vessels.ToList(); - // Spawn in the SpawnProbe at the camera position and switch to it so that we can clean up the other vessels properly. - var dummyVar = EditorFacility.None; - Vector3d dummySpawnCoords; - FlightGlobals.currentMainBody.GetLatLonAlt(FlightCamera.fetch.transform.position + 100f * (FlightCamera.fetch.transform.position - FlightGlobals.currentMainBody.transform.position).normalized, out dummySpawnCoords.x, out dummySpawnCoords.y, out dummySpawnCoords.z); - if (SpawnUtils.spawnProbeLocation == null) yield break; - Vessel spawnProbe = VesselLoader.SpawnVesselFromCraftFile(SpawnUtils.spawnProbeLocation, dummySpawnCoords, 0f, 0f, 0f, out dummyVar); - if (spawnProbe == null) - { - message = "Failed to spawn SpawnProbe!"; - BDACompetitionMode.Instance.competitionStatus.Add(message); - Debug.LogError("[BDArmory.CircularSpawning]: " + message); - yield break; - } - spawnProbe.Landed = false; // Tell KSP that it's not landed so KSP doesn't mess with its position. - yield return new WaitWhile(() => spawnProbe != null && (!spawnProbe.loaded || spawnProbe.packed)); - while (spawnProbe != null && FlightGlobals.ActiveVessel != spawnProbe) - { - LoadedVesselSwitcher.Instance.ForceSwitchVessel(spawnProbe); - yield return waitForFixedUpdate; - } - // Kill all other vessels (including debris). - foreach (var vessel in vesselsToKill) - SpawnUtils.RemoveVessel(vessel); - // Finally, remove the SpawnProbe - SpawnUtils.RemoveVessel(spawnProbe); - SpawnUtils.originalTeams.Clear(); - } - while (SpawnUtils.removingVessels) - yield return waitForFixedUpdate; - #endregion - - #region Spawning - // Get the spawning point in world position coordinates. - var terrainAltitude = FlightGlobals.currentMainBody.TerrainAltitude(spawnConfig.latitude, spawnConfig.longitude); - var spawnPoint = FlightGlobals.currentMainBody.GetWorldSurfacePosition(spawnConfig.latitude, spawnConfig.longitude, terrainAltitude + spawnConfig.altitude); - var radialUnitVector = (spawnPoint - FlightGlobals.currentMainBody.transform.position).normalized; - var localSurfaceNormal = radialUnitVector; - Ray ray; - RaycastHit hit; - - if (spawnConfig.killEverythingFirst) - { - // Update the floating origin offset, so that the vessels spawn within range of the physics. - FloatingOrigin.SetOffset(spawnPoint); // This adjusts local coordinates, such that spawnPoint is (0,0,0). - SpawnUtils.ShowSpawnPoint(spawnConfig.worldIndex, spawnConfig.latitude, spawnConfig.longitude, spawnConfig.altitude, 2 * spawnDistance, true); - // Re-acquire the spawning point after the floating origin shift. - terrainAltitude = FlightGlobals.currentMainBody.TerrainAltitude(spawnConfig.latitude, spawnConfig.longitude); - spawnPoint = FlightGlobals.currentMainBody.GetWorldSurfacePosition(spawnConfig.latitude, spawnConfig.longitude, terrainAltitude + spawnConfig.altitude); - radialUnitVector = (spawnPoint - FlightGlobals.currentMainBody.transform.position).normalized; - - if (terrainAltitude > 0) // Not over the ocean or on a surfaceless body. - { - // Wait for the terrain to load in before continuing. - var testPosition = spawnPoint + 1000f * radialUnitVector; - var terrainDistance = 1000f + (float)spawnConfig.altitude; - var lastTerrainDistance = terrainDistance; - var distanceToCoMainBody = (testPosition - FlightGlobals.currentMainBody.transform.position).magnitude; - ray = new Ray(testPosition, -radialUnitVector); - message = "Waiting up to 10s for terrain to settle."; - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.CircularSpawning]: " + message); - BDACompetitionMode.Instance.competitionStatus.Add(message); - var startTime = Planetarium.GetUniversalTime(); - double lastStableTimeStart = startTime; - double stableTime = 0; - do - { - lastTerrainDistance = terrainDistance; - yield return waitForFixedUpdate; - terrainDistance = Physics.Raycast(ray, out hit, 2f * (float)(spawnConfig.altitude + distanceToCoMainBody), (int)LayerMasks.Scenery) ? hit.distance : -1f; // Oceans shouldn't be more than 10km deep... - if (terrainDistance < 0f) // Raycast is failing to find terrain. - { - if (Planetarium.GetUniversalTime() - startTime < 1) continue; // Give the terrain renderer a chance to spawn the terrain. - else break; - } - if (Math.Abs(lastTerrainDistance - terrainDistance) > 0.1f) - lastStableTimeStart = Planetarium.GetUniversalTime(); // Reset the stable time tracker. - stableTime = Planetarium.GetUniversalTime() - lastStableTimeStart; - } while (Planetarium.GetUniversalTime() - startTime < 10 && stableTime < 1f); - if (terrainDistance < 0) - { - if (!spawnAirborne) - { - message = "Failed to find terrain at the spawning point! Try increasing the spawn altitude."; - Debug.Log("[BDArmory.CircularSpawning]: " + message); - BDACompetitionMode.Instance.competitionStatus.Add(message); - vesselsSpawning = false; - spawnFailureReason = SpawnFailureReason.NoTerrain; - yield break; - } - } - else - { - spawnPoint = hit.point + (float)spawnConfig.altitude * hit.normal; - localSurfaceNormal = hit.normal; - } - } - } - else if ((spawnPoint - FloatingOrigin.fetch.offset).magnitude > 100e3) - { - message = "WARNING The spawn point is " + ((spawnPoint - FloatingOrigin.fetch.offset).magnitude / 1000).ToString("G4") + "km away. Expect vessels to be killed immediately."; - BDACompetitionMode.Instance.competitionStatus.Add(message); - } - - // Spawn the craft in an outward facing ring. - Debug.Log("[BDArmory.CircularSpawning]: Spawning vessels..."); - var spawnedVessels = new Dictionary>(); - Vector3d craftGeoCoords; - Vector3 craftSpawnPosition; - var refDirection = Math.Abs(Vector3.Dot(Vector3.up, radialUnitVector)) < 0.71f ? Vector3.up : Vector3.forward; // Avoid that the reference direction is colinear with the local surface normal. - string failedVessels = ""; - var shipFacility = EditorFacility.None; - List> teamVesselNames = null; - if (spawnConfig.teamsSpecific == null) - { - foreach (var craftUrl in spawnConfig.craftFiles) // First spawn the vessels in the air. - { - var heading = 360f * spawnedVesselCount / spawnConfig.craftFiles.Count; - var direction = Vector3.ProjectOnPlane(Quaternion.AngleAxis(heading, radialUnitVector) * refDirection, radialUnitVector).normalized; - craftSpawnPosition = spawnPoint + 1000f * radialUnitVector + spawnDistance * direction; // Spawn 1000m higher than asked for, then adjust the altitude later once the craft's loaded. - FlightGlobals.currentMainBody.GetLatLonAlt(craftSpawnPosition, out craftGeoCoords.x, out craftGeoCoords.y, out craftGeoCoords.z); // Convert spawn point to geo-coords for the actual spawning function. - Vessel vessel = null; - try - { - vessel = VesselLoader.SpawnVesselFromCraftFile(craftUrl, craftGeoCoords, 0f, 0f, 0f, out shipFacility); // SPAWN - } - catch { vessel = null; } - if (vessel == null) - { - var craftName = Path.GetFileNameWithoutExtension(craftUrl); - Debug.LogWarning("[BDArmory.CircularSpawning]: Failed to spawn craft " + craftUrl); - failedVessels += "\n - " + craftName; - continue; - } - vessel.Landed = false; // Tell KSP that it's not landed so KSP doesn't mess with its position. - vessel.ResumeStaging(); // Trigger staging to resume to get staging icons to work properly. - if (spawnedVessels.ContainsKey(vessel.vesselName)) - { - var count = 1; - var potentialName = vessel.vesselName + "_" + count; - while (spawnedVessels.ContainsKey(potentialName)) - potentialName = vessel.vesselName + "_" + (++count); - vessel.vesselName = potentialName; - } - spawnedVessels.Add(vessel.vesselName, new Tuple(vessel, craftSpawnPosition, direction, vessel.GetHeightFromTerrain() - 35f, shipFacility)); // Store the vessel, its spawning point (which is different from its position) and height from the terrain! - ++spawnedVesselCount; - } - } - else - { - teamVesselNames = new List>(); - var currentTeamNames = new List(); - int spawnedTeamCount = 0; - Vector3 teamSpawnPosition; - foreach (var team in spawnConfig.teamsSpecific) - { - currentTeamNames.Clear(); - var teamHeading = 360f * spawnedTeamCount / spawnConfig.teamsSpecific.Count; - var teamDirection = Vector3.ProjectOnPlane(Quaternion.AngleAxis(teamHeading, radialUnitVector) * refDirection, radialUnitVector).normalized; - teamSpawnPosition = spawnPoint + 1000f * radialUnitVector + spawnDistance * teamDirection; // Spawn 1000m hight than asked for, then adjust the altitude later once the craft's loaded. - int teamSpawnCount = 0; - foreach (var craftUrl in team) - { - var heading = 360f / team.Count * (teamSpawnCount - (team.Count - 1) / 2f) / team.Count + teamHeading; - var direction = Vector3.ProjectOnPlane(Quaternion.AngleAxis(heading, radialUnitVector) * refDirection, radialUnitVector).normalized; - craftSpawnPosition = teamSpawnPosition + spawnDistance / 4f * direction; // Spawn in clusters around the team spawn points. - FlightGlobals.currentMainBody.GetLatLonAlt(craftSpawnPosition, out craftGeoCoords.x, out craftGeoCoords.y, out craftGeoCoords.z); // Convert spawn point to geo-coords for the actual spawning function. - Vessel vessel = null; - try - { - vessel = VesselLoader.SpawnVesselFromCraftFile(craftUrl, craftGeoCoords, 0f, 0f, 0f, out shipFacility); // SPAWN - } - catch { vessel = null; } - if (vessel == null) - { - var craftName = craftUrl.Substring(Path.Combine(KSPUtil.ApplicationRootPath, "AutoSpawn", spawnConfig.folder).Length + 1); - Debug.Log("[BDArmory.CircularSpawning]: Failed to spawn craft " + craftName); - failedVessels += "\n - " + craftName; - continue; - } - vessel.Landed = false; // Tell KSP that it's not landed so KSP doesn't mess with its position. - vessel.ResumeStaging(); // Trigger staging to resume to get staging icons to work properly. - if (spawnedVessels.ContainsKey(vessel.vesselName)) - { - var count = 1; - var potentialName = vessel.vesselName + "_" + count; - while (spawnedVessels.ContainsKey(potentialName)) - potentialName = vessel.vesselName + "_" + (++count); - vessel.vesselName = potentialName; - } - currentTeamNames.Add(vessel.vesselName); - if (spawnConfig.assignTeams) - { - switch (spawnConfig.numberOfTeams) - { - case 1: // Assign team names based on folders. - { - var paths = craftUrl.Split(Path.DirectorySeparatorChar); - SpawnUtils.originalTeams[vessel.vesselName] = paths[paths.Length - 2]; - break; - } - case -1: // Assign team names based on craft filename. We can't use vessel name as that can get adjusted above to avoid conflicts. - { - var paths = craftUrl.Split(Path.DirectorySeparatorChar); - SpawnUtils.originalTeams[vessel.vesselName] = paths[paths.Length - 1].Substring(0, paths[paths.Length - 1].Length - 6); - break; - } - } - } - spawnedVessels.Add(vessel.vesselName, new Tuple(vessel, craftSpawnPosition, direction, vessel.GetHeightFromTerrain() - 35f, shipFacility)); // Store the vessel, its spawning point (which is different from its position) and height from the terrain! - ++spawnedVesselCount; - ++teamSpawnCount; - } - teamVesselNames.Add(currentTeamNames.ToList()); - ++spawnedTeamCount; - } - } - if (failedVessels != "") - { - message = "Some vessels failed to spawn: " + failedVessels; - BDACompetitionMode.Instance.competitionStatus.Add(message); - Debug.Log("[BDArmory.CircularSpawning]: " + message); - } - - // Wait for an update so that the vessels' parts list gets updated. - yield return waitForFixedUpdate; - - // Count the vessels' parts for checking later. - var spawnedVesselPartCounts = new Dictionary(); - foreach (var vesselName in spawnedVessels.Keys) - spawnedVesselPartCounts.Add(vesselName, spawnedVessels[vesselName].Item1.parts.Count); - - // Wait another update so that the reference transforms get updated. - yield return waitForFixedUpdate; - - // Now rotate them and put them at the right altitude. - var finalSpawnPositions = new Dictionary(); - var finalSpawnRotations = new Dictionary(); - { - var startTime = Time.time; - // Sometimes if a vessel camera switch occurs, the craft appears unloaded for a couple of frames. This avoids NREs for control surfaces triggered by the change in reference transform. - while (spawnedVessels.Values.All(vessel => vessel.Item1 != null && (vessel.Item1.ReferenceTransform == null || vessel.Item1.rootPart == null || vessel.Item1.rootPart.GetReferenceTransform() == null)) && (Time.time - startTime < 1f)) yield return waitForFixedUpdate; - var nullVessels = spawnedVessels.Where(kvp => kvp.Value.Item1 == null).Select(kvp => kvp.Key).ToList(); - if (nullVessels.Count() > 0) - { - message = string.Join(", ", nullVessels) + " disappeared during spawning!"; - Debug.Log("[BDArmory.CircularSpawning]: " + message); - BDACompetitionMode.Instance.competitionStatus.Add(message); - vesselsSpawning = false; - spawnFailureReason = SpawnFailureReason.VesselLostParts; - yield break; - } - var noRootVessels = spawnedVessels.Where(kvp => kvp.Value.Item1.rootPart == null).Select(kvp => kvp.Key).ToList(); - if (noRootVessels.Count() > 0) - { - message = string.Join(", ", noRootVessels) + " had no root part during spawning!"; - Debug.Log("[BDArmory.CircularSpawning]: " + message); - BDACompetitionMode.Instance.competitionStatus.Add(message); - vesselsSpawning = false; - spawnFailureReason = SpawnFailureReason.VesselLostParts; - yield break; - } - foreach (var vessel in spawnedVessels.Values.Select(sv => sv.Item1)) - { - vessel.SetReferenceTransform(vessel.rootPart); // Set the reference transform to the root part's transform. - } - } - foreach (var vesselName in spawnedVessels.Keys) - { - var vessel = spawnedVessels[vesselName].Item1; - if (vessel == null) - { - message = "A vessel disappeared during spawning!"; - Debug.Log("[BDArmory.CircularSpawning]: " + message); - BDACompetitionMode.Instance.competitionStatus.Add(message); - vesselsSpawning = false; - spawnFailureReason = SpawnFailureReason.VesselLostParts; - yield break; - } - craftSpawnPosition = spawnedVessels[vesselName].Item2; - var direction = spawnedVessels[vesselName].Item3; - var heightFromTerrain = spawnedVessels[vesselName].Item4; - shipFacility = spawnedVessels[vesselName].Item5; - var localRadialUnitVector = (craftSpawnPosition - FlightGlobals.currentMainBody.transform.position).normalized; - ray = new Ray(craftSpawnPosition, -localRadialUnitVector); - var distanceToCoMainBody = (craftSpawnPosition - FlightGlobals.currentMainBody.transform.position).magnitude; - float distance; - if (terrainAltitude > 0 && Physics.Raycast(ray, out hit, (float)(spawnConfig.altitude + distanceToCoMainBody), (int)LayerMasks.Scenery)) - { - distance = hit.distance; - localSurfaceNormal = hit.normal; - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.CircularSpawning]: found terrain for spawn adjustments"); - } - else - { - distance = FlightGlobals.getAltitudeAtPos(craftSpawnPosition) - (float)terrainAltitude; // If the raycast fails or we're spawning over water, use the value from FlightGlobals and terrainAltitude of the original spawn point. - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.CircularSpawning]: failed to find terrain for spawn adjustments"); - } - - if (!spawnAirborne || BDArmorySettings.SF_GRAVITY) //spawn parallel to ground if surface or space spawning - { - vessel.SetRotation(Quaternion.FromToRotation(shipFacility == EditorFacility.SPH ? -vessel.ReferenceTransform.forward : vessel.ReferenceTransform.up, localSurfaceNormal) * vessel.transform.rotation); // Re-orient the vessel to the terrain normal. - vessel.SetRotation(Quaternion.AngleAxis(Vector3.SignedAngle(shipFacility == EditorFacility.SPH ? vessel.ReferenceTransform.up : -vessel.ReferenceTransform.forward, direction, localSurfaceNormal), localSurfaceNormal) * vessel.transform.rotation); // Re-orient the vessel to the right direction. - } - else - { - vessel.SetRotation(Quaternion.FromToRotation(-vessel.ReferenceTransform.up, localRadialUnitVector) * vessel.transform.rotation); // Re-orient the vessel to the local gravity direction. - vessel.SetRotation(Quaternion.AngleAxis(Vector3.SignedAngle(-vessel.ReferenceTransform.forward, direction, localRadialUnitVector), localRadialUnitVector) * vessel.transform.rotation); // Re-orient the vessel to the right direction. - vessel.SetRotation(Quaternion.AngleAxis(-10f, vessel.ReferenceTransform.right) * vessel.transform.rotation); // Tilt 10° outwards. - } - finalSpawnRotations[vesselName] = vessel.transform.rotation; - if (FlightGlobals.currentMainBody.hasSolidSurface) - finalSpawnPositions[vesselName] = craftSpawnPosition + localRadialUnitVector * (float)(spawnConfig.altitude + heightFromTerrain - distance); - else - finalSpawnPositions[vesselName] = craftSpawnPosition - 1000f * localRadialUnitVector; - if (vessel.mainBody.ocean) // Check for being under water. - { - var distanceUnderWater = -FlightGlobals.getAltitudeAtPos(finalSpawnPositions[vesselName]); - if (distanceUnderWater >= 0) // Under water, move the vessel to the surface. - { - vessel.Splashed = true; // Set the vessel as splashed. - } - } - vessel.SetPosition(finalSpawnPositions[vesselName]); - if (BDArmorySettings.SPACE_HACKS) - { - var SF = vessel.rootPart.FindModuleImplementing(); - if (SF == null) - { - SF = (ModuleSpaceFriction)vessel.rootPart.AddModule("ModuleSpaceFriction"); - } - } - if (BDArmorySettings.MUTATOR_MODE) - { - var MM = vessel.rootPart.FindModuleImplementing(); - if (MM == null) - { - MM = (BDAMutator)vessel.rootPart.AddModule("BDAMutator"); - } - } - if (BDArmorySettings.HACK_INTAKES) - { - SpawnUtils.HackIntakes(vessel, true); - } - Debug.Log("[BDArmory.CircularSpawning]: Vessel " + vessel.vesselName + " spawned!"); - } - #endregion - - #region Post-spawning - yield return waitForFixedUpdate; - // Revert the camera and focus on one as it lowers to the terrain. - SpawnUtils.RevertSpawnLocationCamera(true); - if ((FlightGlobals.ActiveVessel == null || FlightGlobals.ActiveVessel.state == Vessel.State.DEAD) && spawnedVessels.Count > 0) - { - LoadedVesselSwitcher.Instance.ForceSwitchVessel(spawnedVessels.First().Value.Item1); // Update the camera. - FlightCamera.fetch.SetDistance(50); - } - - // Validate vessels and wait for weapon managers to be registered. - var postSpawnCheckStartTime = Planetarium.GetUniversalTime(); - var allWeaponManagersAssigned = false; - var vesselsToCheck = spawnedVessels.Select(v => v.Value.Item1).ToList(); - if (vesselsToCheck.Count > 0) - { - List> invalidVessels; - // Check that the spawned vessels are valid craft - do - { - yield return waitForFixedUpdate; - invalidVessels = vesselsToCheck.Select(vessel => new Tuple(vessel.vesselName, BDACompetitionMode.Instance.IsValidVessel(vessel))).Where(t => t.Item2 != BDACompetitionMode.InvalidVesselReason.None).ToList(); - } while (invalidVessels.Count > 0 && Planetarium.GetUniversalTime() - postSpawnCheckStartTime < 1); // Give it up to 1s for KSP to populate the vessel's AI and WM. - if (invalidVessels.Count > 0) - { - BDACompetitionMode.Instance.competitionStatus.Add("The following vessels are invalid:\n - " + string.Join("\n - ", invalidVessels.Select(t => t.Item1 + " : " + t.Item2))); - Debug.Log("[BDArmory.CircularSpawning]: Invalid vessels: " + string.Join(", ", invalidVessels.Select(t => t.Item1 + ":" + t.Item2))); - spawnFailureReason = SpawnFailureReason.InvalidVessel; - } - else - { - do - { - yield return waitForFixedUpdate; - CheckForRenamedVessels(spawnedVessels); - - // Check that none of the vessels have lost parts. - if (spawnedVessels.Any(kvp => kvp.Value.Item1 == null || kvp.Value.Item1.parts.Count < spawnedVesselPartCounts[kvp.Key])) - { - var offendingVessels = spawnedVessels.Where(kvp => kvp.Value.Item1.parts.Count < spawnedVesselPartCounts[kvp.Key]); - message = "One of the vessels lost parts after spawning: " + string.Join(", ", offendingVessels.Select(kvp => kvp.Value.Item1 != null ? kvp.Value.Item1.vesselName : null)); - BDACompetitionMode.Instance.competitionStatus.Add(message); - Debug.Log("[BDArmory.CircularSpawning]: " + message); - spawnFailureReason = SpawnFailureReason.VesselLostParts; - break; - } - - // Wait for all the weapon managers to be added to LoadedVesselSwitcher. - LoadedVesselSwitcher.Instance.UpdateList(); - var weaponManagers = LoadedVesselSwitcher.Instance.WeaponManagers.SelectMany(tm => tm.Value).ToList(); - foreach (var vessel in vesselsToCheck.ToList()) - { - var weaponManager = VesselModuleRegistry.GetModule(vessel); - if (weaponManager != null && weaponManagers.Contains(weaponManager)) // The weapon manager has been added, let's go! - { - // Turn on the brakes. - spawnedVessels[vessel.vesselName].Item1.ActionGroups.SetGroup(KSPActionGroup.Brakes, false); // Disable them first to make sure they trigger on toggling. - spawnedVessels[vessel.vesselName].Item1.ActionGroups.SetGroup(KSPActionGroup.Brakes, true); - - vesselsToCheck.Remove(vessel); - } - } - if (vesselsToCheck.Count == 0) - allWeaponManagersAssigned = true; - - if (allWeaponManagersAssigned) - { - if (spawnConfig.numberOfTeams != 1 && spawnConfig.numberOfTeams != -1) // Already assigned. - SpawnUtils.SaveTeams(); - break; - } - } while (Planetarium.GetUniversalTime() - postSpawnCheckStartTime < 10); // Give it up to 10s for the weapon managers to get added to the LoadedVesselSwitcher's list. - if (!allWeaponManagersAssigned && spawnFailureReason == SpawnFailureReason.None) - { - BDACompetitionMode.Instance.competitionStatus.Add("Timed out waiting for weapon managers to appear in the Vessel Switcher."); - spawnFailureReason = SpawnFailureReason.TimedOut; - } - } - } - - // Reset craft positions and rotations as sometimes KSP packs and unpacks vessels between frames and resets things! - foreach (var vesselName in spawnedVessels.Keys) - { - if (spawnedVessels[vesselName].Item1 == null) continue; - spawnedVessels[vesselName].Item1.SetPosition(finalSpawnPositions[vesselName]); - spawnedVessels[vesselName].Item1.SetRotation(finalSpawnRotations[vesselName]); - } - - if (allWeaponManagersAssigned) - { - if (spawnConfig.altitude >= 0 && !spawnAirborne) - { - // Prevent the vessels from falling too fast and check if their velocities in the surface normal direction is below a threshold. - var vesselsHaveLanded = spawnedVessels.Keys.ToDictionary(v => v, v => (int)0); // 1=started moving, 2=landed. - var landingStartTime = Planetarium.GetUniversalTime(); - do - { - yield return waitForFixedUpdate; - foreach (var vesselName in spawnedVessels.Keys) - { - var vessel = spawnedVessels[vesselName].Item1; - if (vessel.LandedOrSplashed && Utils.GetRadarAltitudeAtPos(vessel.transform.position) <= 0) // Wait for the vessel to settle a bit in the water. The 15s buffer should be more than sufficient. - { - vesselsHaveLanded[vesselName] = 2; - } - if (vesselsHaveLanded[vesselName] == 0 && Vector3.Dot(vessel.srf_velocity, radialUnitVector) < 0) // Check that vessel has started moving. - vesselsHaveLanded[vesselName] = 1; - if (vesselsHaveLanded[vesselName] == 1 && Vector3.Dot(vessel.srf_velocity, radialUnitVector) >= 0) // Check if the vessel has landed. - { - vesselsHaveLanded[vesselName] = 2; - if (Utils.GetRadarAltitudeAtPos(vessel.transform.position) > 0) - vessel.Landed = true; // Tell KSP that the vessel is landed. - else - vessel.Splashed = true; // Tell KSP that the vessel is splashed. - } - if (vesselsHaveLanded[vesselName] == 1 && vessel.srf_velocity.sqrMagnitude > spawnConfig.easeInSpeed) // While the vessel hasn't landed, prevent it from moving too fast. - vessel.SetWorldVelocity(0.99 * spawnConfig.easeInSpeed * vessel.srf_velocity); // Move at VESSEL_SPAWN_EASE_IN_SPEED m/s at most. - } - - // Check that none of the vessels have lost parts. - if (spawnedVessels.Any(kvp => kvp.Value.Item1.parts.Count < spawnedVesselPartCounts[kvp.Key])) - { - var offendingVessels = spawnedVessels.Where(kvp => kvp.Value.Item1.parts.Count < spawnedVesselPartCounts[kvp.Key]); - message = "One of the vessels lost parts after spawning: " + string.Join(", ", offendingVessels.Select(kvp => kvp.Value.Item1 != null ? kvp.Value.Item1.vesselName : null)); - BDACompetitionMode.Instance.competitionStatus.Add(message); - spawnFailureReason = SpawnFailureReason.VesselLostParts; - break; - } - - if (vesselsHaveLanded.Values.All(v => v == 2)) - { - vesselSpawnSuccess = true; - message = "Vessel spawning SUCCEEDED!"; - BDACompetitionMode.Instance.competitionStatus.Add(message); - break; - } - } while (Planetarium.GetUniversalTime() - landingStartTime < 15 + spawnConfig.altitude / spawnConfig.easeInSpeed); // Give the vessels up to (15 + altitude / VESSEL_SPAWN_EASE_IN_SPEED) seconds to land. - if (!vesselSpawnSuccess && spawnFailureReason == SpawnFailureReason.None) - { - BDACompetitionMode.Instance.competitionStatus.Add("Timed out waiting for the vessels to land."); - spawnFailureReason = SpawnFailureReason.TimedOut; - } - } - else - { - // Check that none of the vessels have lost parts. - if (spawnedVessels.Any(kvp => kvp.Value.Item1.parts.Count < spawnedVesselPartCounts[kvp.Key])) - { - var offendingVessels = spawnedVessels.Where(kvp => kvp.Value.Item1.parts.Count < spawnedVesselPartCounts[kvp.Key]); - message = "One of the vessels lost parts after spawning: " + string.Join(", ", offendingVessels.Select(kvp => kvp.Value.Item1 != null ? kvp.Value.Item1.vesselName : null)); - BDACompetitionMode.Instance.competitionStatus.Add(message); - spawnFailureReason = SpawnFailureReason.VesselLostParts; - } - else - { - foreach (var vessel in spawnedVessels.Select(v => v.Value.Item1)) - { - var weaponManager = VesselModuleRegistry.GetModule(vessel); - if (!weaponManager) continue; // Safety check in case the vessel got destroyed. - - // Activate the vessel with AG10, or failing that, staging. - vessel.ActionGroups.ToggleGroup(BDACompetitionMode.KM_dictAG[10]); // Modular Missiles use lower AGs (1-3) for staging, use a high AG number to not affect them - weaponManager.AI.ActivatePilot(); - weaponManager.AI.CommandTakeOff(); - if (weaponManager.guardMode) - { - Debug.Log($"[BDArmory.CircularSpawning]: Disabling guardMode on {vessel.vesselName}."); - weaponManager.ToggleGuardMode(); // Disable guard mode (in case someone enabled it on AG10 or in the SPH). - weaponManager.SetTarget(null); - } - - if (!BDArmorySettings.NO_ENGINES && SpawnUtils.CountActiveEngines(vessel) == 0) // If the vessel didn't activate their engines on AG10, then activate all their engines and hope for the best. - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.CircularSpawning]: " + vessel.vesselName + " didn't activate engines on AG10! Activating ALL their engines."); - SpawnUtils.ActivateAllEngines(vessel); - } - else if (BDArmorySettings.NO_ENGINES && SpawnUtils.CountActiveEngines(vessel) > 0) // Vessel had some active engines. Turn them off if possible. - { - SpawnUtils.ActivateAllEngines(vessel, false); - } - } - - vesselSpawnSuccess = true; - } - } - foreach (var vessel in spawnedVessels.Select(v => v.Value.Item1)) - vessel.altimeterDisplayState = AltimeterDisplayState.AGL; - } - if (!vesselSpawnSuccess) - { - message = "Vessel spawning FAILED! Reason: " + spawnFailureReason; - BDACompetitionMode.Instance.competitionStatus.Add(message); - } - else - { - CheckForRenamedVessels(spawnedVessels); - if (spawnConfig.assignTeams) - { - // Assign the vessels to teams. - Debug.Log("[BDArmory.CircularSpawning]: Assigning vessels to teams."); - if (spawnConfig.teamsSpecific == null && spawnConfig.teamCounts == null && spawnConfig.numberOfTeams > 1) - { - int numberPerTeam = spawnedVesselCount / spawnConfig.numberOfTeams; - int residue = spawnedVesselCount - numberPerTeam * spawnConfig.numberOfTeams; - spawnConfig.teamCounts = new List(); - for (int team = 0; team < spawnConfig.numberOfTeams; ++team) - spawnConfig.teamCounts.Add(numberPerTeam + (team < residue ? 1 : 0)); - } - LoadedVesselSwitcher.Instance.MassTeamSwitch(true, useOriginalTeamNames, spawnConfig.teamCounts, teamVesselNames); - yield return waitForFixedUpdate; - } - } - - if (BDArmorySettings.RUNWAY_PROJECT) - { - // Check AI/WM counts and placement for RWP. - foreach (var vesselName in spawnedVessels.Keys) - { - SpawnUtils.CheckAIWMCounts(spawnedVessels[vesselName].Item1); - SpawnUtils.CheckAIWMPlacement(spawnedVessels[vesselName].Item1); - } - } - #endregion - - Debug.Log("[BDArmory.CircularSpawning]: Vessel spawning " + (vesselSpawnSuccess ? "SUCCEEDED!" : "FAILED! " + spawnFailureReason)); - vesselsSpawning = false; - } - #endregion - - // TODO Continuous Single Spawning and Team Spawning should probably, at some point, be separated into their own spawn strategies that make use of the above spawning functions. Also, they need cleaning up, which would probably remove the need for some of the SpawnAllVesselsOnce variants above. - #region Continuous Single Spawning - private bool vesselsSpawningOnceContinuously = false; - public Coroutine spawnAllVesselsOnceContinuouslyCoroutine = null; - - public void SpawnAllVesselsOnceContinuously(int worldIndex, double latitude, double longitude, double altitude = 0, float distance = 10f, bool absDistanceOrFactor = false, float easeInSpeed = 1f, bool killEverythingFirst = true, bool assignTeams = true, int numberOfTeams = 0, List teamCounts = null, List> teamsSpecific = null, string spawnFolder = null, List craftFiles = null) - { - SpawnAllVesselsOnceContinuously(new SpawnConfig(worldIndex, latitude, longitude, altitude, distance, absDistanceOrFactor, easeInSpeed, killEverythingFirst, assignTeams, numberOfTeams, teamCounts, teamsSpecific, spawnFolder, craftFiles)); - } - public void SpawnAllVesselsOnceContinuously(SpawnConfig spawnConfig) - { - vesselsSpawningOnceContinuously = true; - if (spawnAllVesselsOnceContinuouslyCoroutine != null) - StopCoroutine(spawnAllVesselsOnceContinuouslyCoroutine); - spawnAllVesselsOnceContinuouslyCoroutine = StartCoroutine(SpawnAllVesselsOnceContinuouslyCoroutine(spawnConfig)); - Debug.Log("[BDArmory.CircularSpawning]: Triggering vessel spawning (continuous single) at " + spawnConfig.latitude.ToString("G6") + ", " + spawnConfig.longitude.ToString("G6") + ", with altitude " + spawnConfig.altitude + "m."); - } - - public IEnumerator SpawnAllVesselsOnceContinuouslyCoroutine(SpawnConfig spawnConfig) - { - while ((vesselsSpawningOnceContinuously) && (BDArmorySettings.VESSEL_SPAWN_CONTINUE_SINGLE_SPAWNING)) - { - SpawnAllVesselsOnce(spawnConfig); - while (vesselsSpawning) - yield return waitForFixedUpdate; - if (!vesselSpawnSuccess) - { - vesselsSpawningOnceContinuously = false; - yield break; - } - yield return waitForFixedUpdate; - - // NOTE: runs in separate coroutine - BDACompetitionMode.Instance.StartCompetitionMode(BDArmorySettings.COMPETITION_DISTANCE); - yield return waitForFixedUpdate; // Give the competition start a frame to get going. - - // start timer coroutine for the duration specified in settings UI - var duration = Core.BDArmorySettings.COMPETITION_DURATION * 60f; - message = "Starting " + (duration > 0 ? "a " + duration.ToString("F0") + "s" : "an unlimited") + " duration competition."; - Debug.Log("[BDArmory.CircularSpawning]: " + message); - BDACompetitionMode.Instance.competitionStatus.Add(message); - while (BDACompetitionMode.Instance.competitionStarting) - yield return waitForFixedUpdate; // Wait for the competition to actually start. - if (!BDACompetitionMode.Instance.competitionIsActive) - { - var message = "Competition failed to start."; - BDACompetitionMode.Instance.competitionStatus.Add(message); - Debug.Log("[BDArmory.CircularSpawning]: " + message); - vesselsSpawningOnceContinuously = false; - yield break; - } - while (BDACompetitionMode.Instance.competitionIsActive) // Wait for the competition to finish (limited duration and log dumping is handled directly by the competition now). - yield return new WaitForSeconds(1); - - // Wait 10s for any user action - double startTime = Planetarium.GetUniversalTime(); - if ((vesselsSpawningOnceContinuously) && (BDArmorySettings.VESSEL_SPAWN_CONTINUE_SINGLE_SPAWNING)) - { - while ((Planetarium.GetUniversalTime() - startTime) < BDArmorySettings.TOURNAMENT_DELAY_BETWEEN_HEATS) - { - BDACompetitionMode.Instance.competitionStatus.Add("Waiting " + (BDArmorySettings.TOURNAMENT_DELAY_BETWEEN_HEATS - (Planetarium.GetUniversalTime() - startTime)).ToString("0") + "s, then respawning pilots"); - yield return new WaitForSeconds(1); - } - } - } - vesselsSpawningOnceContinuously = false; // For when VESSEL_SPAWN_CONTINUE_SINGLE_SPAWNING gets toggled. - } - - /// - /// If the VESSELNAMING tag exists in the craft file, then KSP renames the vessel at some point after spawning. - /// This function checks for renamed vessels and sets the name back to what it was. - /// This must be called once after a yield, before using vessel.vesselName as an index in spawnedVessels.Keys. - /// - /// - void CheckForRenamedVessels(Dictionary> spawnedVessels) - { - foreach (var vesselName in spawnedVessels.Keys.ToList()) - { - if (vesselName != spawnedVessels[vesselName].Item1.vesselName) - { - spawnedVessels[vesselName].Item1.vesselName = vesselName; - } - } - } - #endregion - - #region Team Spawning - /// - /// Spawn multiple groups of vessels using the CircularSpawning using multiple SpawnConfigs. - /// - /// - /// - /// - /// - public void TeamSpawn(List spawnConfigs, bool startCompetition = false, double competitionStartDelay = 0d, bool startCompetitionNow = false) - { - vesselsSpawning = true; // Indicate that vessels are spawning here to avoid timing issues with Update in other modules. - SpawnUtils.RevertSpawnLocationCamera(true); - if (teamSpawnCoroutine != null) - StopCoroutine(teamSpawnCoroutine); - teamSpawnCoroutine = StartCoroutine(TeamsSpawnCoroutine(spawnConfigs, startCompetition, competitionStartDelay, startCompetitionNow)); - } - private Coroutine teamSpawnCoroutine; - public IEnumerator TeamsSpawnCoroutine(List spawnConfigs, bool startCompetition = false, double competitionStartDelay = 0d, bool startCompetitionNow = false) - { - bool killAllFirst = true; - List spawnCounts = new List(); - spawnFailureReason = SpawnFailureReason.None; - // Spawn each team. - foreach (var spawnConfig in spawnConfigs) - { - vesselsSpawning = true; // Gets set to false each time spawning is finished, so we need to re-enable it again. - vesselSpawnSuccess = false; - yield return SpawnAllVesselsOnceCoroutine(spawnConfig.worldIndex, spawnConfig.latitude, spawnConfig.longitude, spawnConfig.altitude, spawnConfig.distance, spawnConfig.absDistanceOrFactor, BDArmorySettings.VESSEL_SPAWN_EASE_IN_SPEED, killAllFirst, false, 0, null, null, spawnConfig.folder, spawnConfig.craftFiles); - if (!vesselSpawnSuccess) - { - message = "Vessel spawning failed, aborting."; - BDACompetitionMode.Instance.competitionStatus.Add(message); - Debug.Log("[BDArmory.CircularSpawning]: " + message); - yield break; - } - spawnCounts.Add(spawnedVesselCount); - // LoadedVesselSwitcher.Instance.MassTeamSwitch(false); // Reset everyone to team 'A' so that the order doesn't get messed up. - killAllFirst = false; - } - yield return waitForFixedUpdate; - SpawnUtils.SaveTeams(); // Save the teams in case they've been pre-configured. - LoadedVesselSwitcher.Instance.MassTeamSwitch(false, false, spawnCounts); // Assign teams based on the groups of spawns. Right click the 'T' to revert to the original team names if they were defined. - if (startCompetition) // Start the competition. - { - var competitionStartDelayStart = Planetarium.GetUniversalTime(); - while (Planetarium.GetUniversalTime() - competitionStartDelayStart < competitionStartDelay - Time.fixedDeltaTime) - { - var timeLeft = competitionStartDelay - (Planetarium.GetUniversalTime() - competitionStartDelayStart); - if ((int)(timeLeft - Time.fixedDeltaTime) < (int)timeLeft) - BDACompetitionMode.Instance.competitionStatus.Add("Competition starting in T-" + timeLeft.ToString("0") + "s"); - yield return waitForFixedUpdate; - } - BDACompetitionMode.Instance.StartCompetitionMode(BDArmorySettings.COMPETITION_DISTANCE); - if (startCompetitionNow) - { - yield return waitForFixedUpdate; - BDACompetitionMode.Instance.StartCompetitionNow(); - } - } - } - #endregion - - } -} \ No newline at end of file diff --git a/BDArmory/Competition/VesselSpawning/ContinuousSpawning.cs b/BDArmory/Competition/VesselSpawning/ContinuousSpawning.cs deleted file mode 100644 index 9cde5a7bc..000000000 --- a/BDArmory/Competition/VesselSpawning/ContinuousSpawning.cs +++ /dev/null @@ -1,647 +0,0 @@ -using UnityEngine; -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Linq; - -using BDArmory.Core; -using BDArmory.Core.Utils; -using BDArmory.Misc; -using BDArmory.Modules; -using BDArmory.UI; - -namespace BDArmory.Competition.VesselSpawning -{ - /// - /// Continous spawning in an airborne ring cycling through all the vessels in the spawn folder. - /// - /// TODO: This should probably be subsumed into its own spawn strategy eventually. - /// The central block of the SpawnVesselsContinuouslyCoroutine function should eventually switch to using SingleVesselSpawning.Instance.SpawnVessel (plus local coroutines for the extra stuff) to do the actual spawning of the vessels once that's ready. - /// - [KSPAddon(KSPAddon.Startup.Flight, false)] - public class ContinuousSpawning : VesselSpawner - { - public static ContinuousSpawning Instance; - - public bool vesselsSpawningContinuously = false; - int continuousSpawnedVesselCount = 0; - private string message; - - void Awake() - { - if (Instance != null) Destroy(Instance); - Instance = this; - } - - public override IEnumerator Spawn(SpawnConfig spawnConfig) => SpawnVesselsContinuouslyAsCoroutine(spawnConfig); - - public void CancelSpawning() - { - // Continuous spawn - if (vesselsSpawningContinuously) - { - vesselsSpawningContinuously = false; - if (continuousSpawningScores != null) - DumpContinuousSpawningScores(); - continuousSpawningScores = null; - message = "Continuous vessel spawning cancelled."; - Debug.Log("[BDArmory.VesselSpawner]: " + message); - BDACompetitionMode.Instance.competitionStatus.Add(message); - BDACompetitionMode.Instance.ResetCompetitionStuff(); - } - if (spawnVesselsContinuouslyCoroutine != null) - { - StopCoroutine(spawnVesselsContinuouslyCoroutine); - spawnVesselsContinuouslyCoroutine = null; - } - } - - public override void PreSpawnInitialisation(SpawnConfig spawnConfig) - { - base.PreSpawnInitialisation(spawnConfig); - - vesselsSpawningContinuously = true; - spawnFailureReason = SpawnFailureReason.None; - continuousSpawningScores = new Dictionary(); - if (spawnVesselsContinuouslyCoroutine != null) - StopCoroutine(spawnVesselsContinuouslyCoroutine); - } - - public void SpawnVesselsContinuously(int worldIndex, double latitude, double longitude, double altitude = 1000, float distance = 20f, bool absDistanceOrFactor = false, bool killEverythingFirst = true, string spawnFolder = null) - { SpawnVesselsContinuously(new SpawnConfig(worldIndex, latitude, longitude, altitude, distance, absDistanceOrFactor, BDArmorySettings.VESSEL_SPAWN_EASE_IN_SPEED, killEverythingFirst, true, 1, null, null, spawnFolder)); } - - public void SpawnVesselsContinuously(SpawnConfig spawnConfig) - { - PreSpawnInitialisation(spawnConfig); - Debug.Log("[BDArmory.VesselSpawner]: Triggering continuous vessel spawning at " + spawnConfig.latitude.ToString("G6") + ", " + spawnConfig.longitude.ToString("G6") + ", with altitude " + spawnConfig.altitude + "m."); - spawnVesselsContinuouslyCoroutine = StartCoroutine(SpawnVesselsContinuouslyCoroutine(spawnConfig)); - } - - /// - /// A coroutine version of the SpawnAllVesselsContinuously function that performs the required prespawn initialisation. - /// - /// The spawn config to use. - public IEnumerator SpawnVesselsContinuouslyAsCoroutine(SpawnConfig spawnConfig) - { - PreSpawnInitialisation(spawnConfig); - Debug.Log("[BDArmory.VesselSpawner]: Triggering continuous vessel spawning at " + spawnConfig.latitude.ToString("G6") + ", " + spawnConfig.longitude.ToString("G6") + ", with altitude " + spawnConfig.altitude + "m."); - yield return SpawnVesselsContinuouslyCoroutine(spawnConfig); - } - - private Coroutine spawnVesselsContinuouslyCoroutine; - HashSet vesselsToActivate = new HashSet(); - // Spawns all vessels in a downward facing ring and activates them (autopilot and AG10, then stage if no engines are firing), then respawns any that die. An altitude of 1000m should be plenty. - // Note: initial vessel separation tends towards 2*pi*spawnDistanceFactor from above for >3 vessels. - private IEnumerator SpawnVesselsContinuouslyCoroutine(SpawnConfig spawnConfig) - { - #region Initialisation and sanity checks - // Tally up the craft to spawn. - if (spawnConfig.craftFiles == null) // Prioritise the list of craftFiles if we're given them. - spawnConfig.craftFiles = Directory.GetFiles(Path.Combine(KSPUtil.ApplicationRootPath, "AutoSpawn", spawnConfig.folder), "*.craft").ToList(); - if (spawnConfig.craftFiles.Count == 0) - { - message = "Vessel spawning: found no craft files in " + Path.Combine(KSPUtil.ApplicationRootPath, "AutoSpawn", spawnConfig.folder); - Debug.Log("[BDArmory.VesselSpawner]: " + message); - BDACompetitionMode.Instance.competitionStatus.Add(message); - vesselsSpawningContinuously = false; - spawnFailureReason = SpawnFailureReason.NoCraft; - yield break; - } - spawnConfig.craftFiles.Shuffle(); // Randomise the spawn order. - spawnConfig.altitude = Math.Max(100, spawnConfig.altitude); // Don't spawn too low. - var spawnDistance = spawnConfig.craftFiles.Count > 1 ? (spawnConfig.absDistanceOrFactor ? spawnConfig.distance : spawnConfig.distance * (1 + (BDArmorySettings.VESSEL_SPAWN_CONCURRENT_VESSELS > 0 ? Math.Min(spawnConfig.craftFiles.Count, BDArmorySettings.VESSEL_SPAWN_CONCURRENT_VESSELS) : spawnConfig.craftFiles.Count))) : 0f; // If it's a single craft, spawn it at the spawn point. - continuousSpawnedVesselCount = 0; // Reset our spawned vessel count. - if (BDArmorySettings.VESSEL_SPAWN_CONCURRENT_VESSELS == 0) - message = "Spawning " + spawnConfig.craftFiles.Count + " vessels at an altitude of " + spawnConfig.altitude.ToString("G0") + (spawnConfig.craftFiles.Count > 8 ? "m, this may take some time..." : "m."); - else - message = "Spawning " + Math.Min(BDArmorySettings.VESSEL_SPAWN_CONCURRENT_VESSELS, spawnConfig.craftFiles.Count) + " of " + spawnConfig.craftFiles.Count + " vessels at an altitude of " + spawnConfig.altitude.ToString("G0") + "m with rolling-spawning."; - Debug.Log("[BDArmory.VesselSpawner]: " + message); - if (BDACompetitionMode.Instance) // Reset competition stuff. - { - BDACompetitionMode.Instance.competitionStatus.Add(message); - BDACompetitionMode.Instance.LogResults("due to continuous spawning", "auto-dump-from-spawning"); // Log results first. - BDACompetitionMode.Instance.StopCompetition(); - BDACompetitionMode.Instance.ResetCompetitionStuff(); // Reset competition scores. - } - vesselsToActivate.Clear(); // Clear any pending vessel activations. - yield return waitForFixedUpdate; - #endregion - - #region Pre-spawning - if (spawnConfig.killEverythingFirst) - { - yield return SpawnUtils.RemoveAllVessels(); - } - #endregion - - #region Spawning - // Get the spawning point in world position coordinates. - var terrainAltitude = FlightGlobals.currentMainBody.TerrainAltitude(spawnConfig.latitude, spawnConfig.longitude); - var spawnPoint = FlightGlobals.currentMainBody.GetWorldSurfacePosition(spawnConfig.latitude, spawnConfig.longitude, terrainAltitude + spawnConfig.altitude); - var radialUnitVector = (spawnPoint - FlightGlobals.currentMainBody.transform.position).normalized; - Ray ray; - RaycastHit hit; - - if (spawnConfig.killEverythingFirst) - { - // Update the floating origin offset, so that the vessels spawn within range of the physics. The terrain takes several frames to load, so we need to wait for the terrain to settle. - FloatingOrigin.SetOffset(spawnPoint); // This adjusts local coordinates, such that spawnPoint is (0,0,0). - SpawnUtils.ShowSpawnPoint(spawnConfig.worldIndex, spawnConfig.latitude, spawnConfig.longitude, spawnConfig.altitude, 2 * spawnDistance, true); - - if (terrainAltitude > 0) // Not over the ocean or on a surfaceless body. - { - // Wait for the terrain to load in before continuing. - var testPosition = 1000f * radialUnitVector; - var terrainDistance = 1000f + (float)spawnConfig.altitude; - var lastTerrainDistance = terrainDistance; - var distanceToCoMainBody = (testPosition - FlightGlobals.currentMainBody.transform.position).magnitude; - ray = new Ray(testPosition, -radialUnitVector); - message = "Waiting up to 10s for terrain to settle."; - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.VesselSpawner]: " + message); - BDACompetitionMode.Instance.competitionStatus.Add(message); - var startTime = Planetarium.GetUniversalTime(); - double lastStableTimeStart = startTime; - double stableTime = 0; - do - { - lastTerrainDistance = terrainDistance; - yield return waitForFixedUpdate; - terrainDistance = Physics.Raycast(ray, out hit, 2f * (float)(spawnConfig.altitude + distanceToCoMainBody), (int)LayerMasks.Scenery) ? hit.distance : -1f; // Oceans shouldn't be more than 10km deep... - if (terrainDistance < 0f || Math.Abs(lastTerrainDistance - terrainDistance) > 0.1f) - lastStableTimeStart = Planetarium.GetUniversalTime(); // Reset the stable time tracker. - stableTime = Planetarium.GetUniversalTime() - lastStableTimeStart; - } while (Planetarium.GetUniversalTime() - startTime < 10 && stableTime < 1f); - } - } - else if ((spawnPoint - FloatingOrigin.fetch.offset).magnitude > 100e3) - { - message = "WARNING The spawn point is " + ((spawnPoint - FloatingOrigin.fetch.offset).magnitude / 1000).ToString("G4") + "km away. Expect vessels to be killed immediately."; - BDACompetitionMode.Instance.competitionStatus.Add(message); - } - - var craftURLToVesselName = new Dictionary(); - var invalidVesselCount = new Dictionary(); - Vector3d craftGeoCoords; - Vector3 craftSpawnPosition; - var shipFacility = EditorFacility.None; - var refDirection = Math.Abs(Vector3.Dot(Vector3.up, radialUnitVector)) < 0.71f ? Vector3.up : Vector3.forward; // Avoid that the reference direction is colinear with the local surface normal. - var geeDirection = FlightGlobals.getGeeForceAtPosition(Vector3.zero); - var spawnSlots = OptimiseSpawnSlots(BDArmorySettings.VESSEL_SPAWN_CONCURRENT_VESSELS > 0 ? Math.Min(spawnConfig.craftFiles.Count, BDArmorySettings.VESSEL_SPAWN_CONCURRENT_VESSELS) : spawnConfig.craftFiles.Count); - var spawnCounts = spawnConfig.craftFiles.ToDictionary(c => c, c => 0); - var spawnQueue = new Queue(); - var craftToSpawn = new Queue(); - bool initialSpawn = true; - double currentUpdateTick; - while (vesselsSpawningContinuously) - { - currentUpdateTick = BDACompetitionMode.Instance.nextUpdateTick; - // Reacquire the spawn point as the local coordinate system may have changed (floating origin adjustments, local body rotation, etc.). - spawnPoint = FlightGlobals.currentMainBody.GetWorldSurfacePosition(spawnConfig.latitude, spawnConfig.longitude, terrainAltitude + spawnConfig.altitude); - radialUnitVector = (spawnPoint - FlightGlobals.currentMainBody.transform.position).normalized; - // Check if sliders have changed. - if (spawnSlots.Count != (BDArmorySettings.VESSEL_SPAWN_CONCURRENT_VESSELS > 0 ? Math.Min(spawnConfig.craftFiles.Count, BDArmorySettings.VESSEL_SPAWN_CONCURRENT_VESSELS) : spawnConfig.craftFiles.Count)) - { - spawnSlots = OptimiseSpawnSlots(BDArmorySettings.VESSEL_SPAWN_CONCURRENT_VESSELS > 0 ? Math.Min(spawnConfig.craftFiles.Count, BDArmorySettings.VESSEL_SPAWN_CONCURRENT_VESSELS) : spawnConfig.craftFiles.Count); - continuousSpawnedVesselCount %= spawnSlots.Count; - } - // Add any craft that hasn't been spawned or has died to the spawn queue if it isn't already in the queue. Note: we need to also check that the vessel isn't null as Unity makes it a fake null! - foreach (var craftURL in spawnConfig.craftFiles.Where(craftURL => (BDArmorySettings.VESSEL_SPAWN_LIVES_PER_VESSEL > 0 ? spawnCounts[craftURL] < BDArmorySettings.VESSEL_SPAWN_LIVES_PER_VESSEL : true) && !spawnQueue.Contains(craftURL) && (!craftURLToVesselName.ContainsKey(craftURL) || (BDACompetitionMode.Instance.Scores.Players.Contains(craftURLToVesselName[craftURL]) && BDACompetitionMode.Instance.Scores.ScoreData[craftURLToVesselName[craftURL]].deathTime >= 0)))) - { - spawnQueue.Enqueue(craftURL); - ++spawnCounts[craftURL]; - } - LoadedVesselSwitcher.Instance.UpdateList(); - var currentlyActive = LoadedVesselSwitcher.Instance.WeaponManagers.SelectMany(tm => tm.Value).ToList().Count; - if (spawnQueue.Count + vesselsToActivate.Count == 0 && currentlyActive < 2)// Nothing left to spawn or activate and only 1 vessel surviving. Time to call it quits and let the competition end. - { - message = "Spawn queue is empty and not enough vessels are active, ending competition."; - Debug.Log("[BDArmory.VesselSpawner]: " + message); - BDACompetitionMode.Instance.StopCompetition(); - break; - } - while (craftToSpawn.Count + vesselsToActivate.Count + currentlyActive < spawnSlots.Count && spawnQueue.Count > 0) - craftToSpawn.Enqueue(spawnQueue.Dequeue()); - if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - var missing = spawnConfig.craftFiles.Where(craftURL => craftURLToVesselName.ContainsKey(craftURL) && !craftToSpawn.Contains(craftURL) && !FlightGlobals.Vessels.Where(v => !VesselModuleRegistry.ignoredVesselTypes.Contains(v.vesselType) && VesselModuleRegistry.GetModuleCount(v) > 0).Select(v => v.vesselName).Contains(craftURLToVesselName[craftURL])).ToList(); - if (missing.Count > 0) - { - Debug.Log("[BDArmory.VesselSpawner]: MISSING vessels: " + string.Join(", ", craftURLToVesselName.Where(c => missing.Contains(c.Key)).Select(c => c.Value))); - } - } - if (craftToSpawn.Count > 0) - { - VesselModuleRegistry.CleanRegistries(); // Clean out any old entries. - // Spawn the craft in a downward facing ring. - string failedVessels = ""; - foreach (var craftURL in craftToSpawn) - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log($"[BDArmory.VesselSpawner]: Spawning vessel from {craftURL}"); - var heading = 360f * spawnSlots[continuousSpawnedVesselCount] / spawnSlots.Count; - var direction = Vector3.ProjectOnPlane(Quaternion.AngleAxis(heading, radialUnitVector) * refDirection, radialUnitVector).normalized; - craftSpawnPosition = spawnPoint + spawnDistance * direction; - FlightGlobals.currentMainBody.GetLatLonAlt(craftSpawnPosition, out craftGeoCoords.x, out craftGeoCoords.y, out craftGeoCoords.z); // Convert spawn point to geo-coords for the actual spawning function. - Vessel vessel = null; - try - { - vessel = VesselLoader.SpawnVesselFromCraftFile(craftURL, craftGeoCoords, 0f, 0f, 0f, out shipFacility); // SPAWN - } - catch { vessel = null; } - if (vessel == null) - { - var craftName = craftURL.Substring(Path.Combine(KSPUtil.ApplicationRootPath, "AutoSpawn", spawnConfig.folder).Length + 1); - Debug.Log("[BDArmory.VesselSpawner]: Failed to spawn craft " + craftName); - failedVessels += "\n - " + craftName; - continue; - } - vessel.Landed = false; // Tell KSP that it's not landed. - vessel.ResumeStaging(); // Trigger staging to resume to get staging icons to work properly. - if (!craftURLToVesselName.ContainsKey(craftURL)) - { - if (craftURLToVesselName.ContainsValue(vessel.vesselName)) // Avoid duplicate names. - { - var count = 1; - var potentialName = vessel.vesselName + "_" + count; - while (craftURLToVesselName.ContainsValue(potentialName)) - potentialName = vessel.vesselName + "_" + (++count); - vessel.vesselName = potentialName; - } - craftURLToVesselName.Add(craftURL, vessel.vesselName); // Store the craftURL -> vessel name. - } - vessel.vesselName = craftURLToVesselName[craftURL]; // Assign the same (potentially modified) name to the craft each time. - // If a competition is active, update the scoring structure. - if ((BDACompetitionMode.Instance.competitionStarting || BDACompetitionMode.Instance.competitionIsActive) && !BDACompetitionMode.Instance.Scores.Players.Contains(vessel.vesselName)) - { - BDACompetitionMode.Instance.Scores.AddPlayer(vessel); - } - if (!vesselsToActivate.Contains(vessel)) - vesselsToActivate.Add(vessel); - if (!continuousSpawningScores.ContainsKey(vessel.vesselName)) - continuousSpawningScores.Add(vessel.vesselName, new ContinuousSpawningScores()); - continuousSpawningScores[vessel.vesselName].vessel = vessel; // Update some values in the scoring structure. - continuousSpawningScores[vessel.vesselName].outOfAmmoTime = 0; - ++continuousSpawnedVesselCount; - continuousSpawnedVesselCount %= spawnSlots.Count; - Debug.Log("[BDArmory.VesselSpawner]: Vessel " + vessel.vesselName + " spawned!"); - BDACompetitionMode.Instance.competitionStatus.Add("Spawned " + vessel.vesselName); - } - craftToSpawn.Clear(); // Clear the queue since we just spawned all those vessels. - if (failedVessels != "") - { - message = "Some vessels failed to spawn, aborting: " + failedVessels; - Debug.Log("[BDArmory.VesselSpawner]: " + message); - BDACompetitionMode.Instance.competitionStatus.Add(message); - spawnFailureReason = SpawnFailureReason.VesselFailedToSpawn; - break; - } - - // Wait for a couple of updates so that the spawned vessels' parts list and reference transform gets updated. - yield return waitForFixedUpdate; - yield return waitForFixedUpdate; - - // Fix control point orientation by setting the reference transformations to that of the root parts and re-orient the vessels accordingly. - foreach (var vessel in vesselsToActivate) - { - int count = 0; - // Sometimes if a vessel camera switch occurs, the craft appears unloaded for a couple of frames. This avoids NREs for control surfaces triggered by the change in reference transform. - while (vessel != null && vessel.rootPart != null && (vessel.ReferenceTransform == null || vessel.rootPart.GetReferenceTransform() == null) && ++count < 5) yield return waitForFixedUpdate; - if (vessel == null || vessel.rootPart == null) - { - Debug.Log($"[BDArmory.VesselSpawner]: " + (vessel == null ? "Spawned vessel was destroyed before it could be activated!" : $"{vessel.vesselName} has no root part!")); - continue; // In case the vessel got destroyed in the mean time. - } - vessel.SetReferenceTransform(vessel.rootPart); - if (BDArmorySettings.SF_GRAVITY) - { - vessel.SetRotation(Quaternion.FromToRotation(shipFacility == EditorFacility.SPH ? -vessel.ReferenceTransform.forward : vessel.ReferenceTransform.up, -geeDirection) * vessel.transform.rotation); // Re-orient the vessel to the gravity direction. - vessel.SetRotation(Quaternion.AngleAxis(Vector3.SignedAngle(shipFacility == EditorFacility.SPH ? vessel.ReferenceTransform.up : -vessel.ReferenceTransform.forward, vessel.transform.position - spawnPoint, -geeDirection), -geeDirection) * vessel.transform.rotation); // Re-orient the vessel to the point outwards. - } - else - { - vessel.SetRotation(Quaternion.FromToRotation(-vessel.ReferenceTransform.up, -geeDirection) * vessel.transform.rotation); // Re-orient the vessel to the local gravity direction. - vessel.SetRotation(Quaternion.AngleAxis(Vector3.SignedAngle(-vessel.ReferenceTransform.forward, vessel.transform.position - spawnPoint, -geeDirection), -geeDirection) * vessel.transform.rotation); // Re-orient the vessel to the right direction. - vessel.SetRotation(Quaternion.AngleAxis(-10f, vessel.ReferenceTransform.right) * vessel.transform.rotation); // Tilt 10° outwards. - } - if (BDArmorySettings.SPACE_HACKS) - { - var SF = vessel.rootPart.FindModuleImplementing(); - if (SF == null) - { - SF = (ModuleSpaceFriction)vessel.rootPart.AddModule("ModuleSpaceFriction"); - } - } - } - } - // Activate the AI and fire up any new weapon managers that appeared. - if (vesselsToActivate.Count > 0) - { - // Wait for an update so that the spawned vessels' FindPart... functions have time to have their internal data updated. - yield return waitForFixedUpdate; - - var weaponManagers = LoadedVesselSwitcher.Instance.WeaponManagers.SelectMany(tm => tm.Value).ToList(); - var vesselsToCheck = vesselsToActivate.ToList(); // Take a copy to avoid modifying the original while iterating over it. - foreach (var vessel in vesselsToCheck) - { - if (!vessel.loaded || vessel.packed) continue; - // Make sure the vessel module registry is up to date. - VesselModuleRegistry.OnVesselModified(vessel, true); - // Check that the vessel is valid. - var invalidReason = BDACompetitionMode.Instance.IsValidVessel(vessel); - if (invalidReason != BDACompetitionMode.InvalidVesselReason.None) - { - bool killIt = false; - var craftURL = craftURLToVesselName.ToDictionary(i => i.Value, i => i.Key)[vessel.vesselName]; - if (invalidVesselCount.ContainsKey(craftURL)) - ++invalidVesselCount[craftURL]; - else - invalidVesselCount.Add(craftURL, 1); - if (invalidVesselCount[craftURL] == 3) // After 3 attempts try spawning it again. - { - message = vessel.vesselName + " is INVALID due to " + invalidReason + ", attempting to respawn it."; - craftToSpawn.Enqueue(craftURL); // Re-add the craft to the vessels to spawn. - killIt = true; - } - if (invalidVesselCount[craftURL] > 5) // After 3 more attempts, mark it as defunct. - { - message = vessel.vesselName + " is STILL INVALID due to " + invalidReason + ", removing it."; - spawnConfig.craftFiles.Remove(craftURL); - killIt = true; - } - if (killIt) - { - BDACompetitionMode.Instance.competitionStatus.Add(message); - Debug.Log("[BDArmory.VesselSpawner]: " + message); - vesselsToActivate.Remove(vessel); - BDACompetitionMode.Instance.Scores.RemovePlayer(vessel.vesselName); - SpawnUtils.RemoveVessel(vessel); // Remove the vessel - } - continue; - } - - // Check if the weapon manager has been added to the weapon managers list. - var weaponManager = VesselModuleRegistry.GetModule(vessel); - if (weaponManager != null && weaponManagers.Contains(weaponManager)) // The weapon manager has been added, let's go! - { - // Activate the vessel with AG10. - vessel.ActionGroups.ToggleGroup(BDACompetitionMode.KM_dictAG[10]); // Modular Missiles use lower AGs (1-3) for staging, use a high AG number to not affect them - weaponManager.AI.ActivatePilot(); - weaponManager.AI.CommandTakeOff(); - if (!BDArmorySettings.NO_ENGINES && SpawnUtils.CountActiveEngines(vessel) == 0) // If the vessel didn't activate their engines on AG10, then activate all their engines and hope for the best. - { - Debug.Log("[BDArmory.VesselSpawner]: " + vessel.vesselName + " didn't activate engines on AG10! Activating ALL their engines."); - SpawnUtils.ActivateAllEngines(vessel); - } - else if (BDArmorySettings.NO_ENGINES && SpawnUtils.CountActiveEngines(vessel) > 0) // Vessel had some active engines. Turn them off if possible. - { - SpawnUtils.ActivateAllEngines(vessel, false); - } - if (BDArmorySettings.TAG_MODE && !string.IsNullOrEmpty(BDACompetitionMode.Instance.Scores.currentlyIT)) - { weaponManager.SetTeam(BDTeam.Get("NO")); } - else - { - // Assign the vessel to an unassigned team. - var currentTeams = weaponManagers.Where(wm => wm != weaponManager).Select(wm => wm.Team).ToHashSet(); // Current teams, excluding us. - char team = 'A'; - while (currentTeams.Contains(BDTeam.Get(team.ToString()))) - ++team; - weaponManager.SetTeam(BDTeam.Get(team.ToString())); - } - weaponManager.ForceScan(); - var craftURL = craftURLToVesselName.ToDictionary(i => i.Value, i => i.Key)[vessel.vesselName]; - // Enable guard mode if a competition is active. - if (BDACompetitionMode.Instance.competitionIsActive) - if (!weaponManager.guardMode) - weaponManager.ToggleGuardMode(); - weaponManager.AI.ReleaseCommand(); - vessel.altimeterDisplayState = AltimeterDisplayState.AGL; - // Adjust BDACompetitionMode's scoring structures. - UpdateCompetitionScores(vessel, true); - ++continuousSpawningScores[vessel.vesselName].spawnCount; - if (invalidVesselCount.ContainsKey(craftURL))// Reset the invalid spawn counter. - invalidVesselCount.Remove(craftURL); - // Update the ramming information for the new vessel. - if (BDACompetitionMode.Instance.rammingInformation != null) - { BDACompetitionMode.Instance.AddPlayerToRammingInformation(vessel); } - vesselsToActivate.Remove(vessel); - SpawnUtils.RevertSpawnLocationCamera(true); // Undo the camera adjustment and reset the camera distance. This has an internal check so that it only occurs once. - if (initialSpawn || FlightGlobals.ActiveVessel == null || FlightGlobals.ActiveVessel.state == Vessel.State.DEAD) - { - initialSpawn = false; - LoadedVesselSwitcher.Instance.ForceSwitchVessel(vessel); // Update the camera. - FlightCamera.fetch.SetDistance(50); - } - } - if (BDArmorySettings.MUTATOR_MODE) - { - var MM = vessel.rootPart.FindModuleImplementing(); - if (MM == null) - { - MM = (BDAMutator)vessel.rootPart.AddModule("BDAMutator"); - } - if (BDArmorySettings.MUTATOR_LIST.Count > 0) - { - if (BDArmorySettings.MUTATOR_APPLY_GLOBAL) //same mutator for all craft - { - MM.EnableMutator(BDACompetitionMode.Instance.currentMutator); - } - if (!BDArmorySettings.MUTATOR_APPLY_GLOBAL && BDArmorySettings.MUTATOR_APPLY_TIMER) //mutator applied on a per-craft basis - { - MM.EnableMutator(); //random mutator - } - } - } - if (BDArmorySettings.HACK_INTAKES) - { - SpawnUtils.HackIntakes(vessel, true); - } - } - } - - // Kill off vessels that are out of ammo for too long if we're in continuous spawning mode and a competition is active. - if (BDACompetitionMode.Instance.competitionIsActive) - KillOffOutOfAmmoVessels(); - - // Wait for any pending vessel removals. - while (SpawnUtils.removingVessels) - yield return waitForFixedUpdate; - - if (BDACompetitionMode.Instance.competitionIsActive) - { - yield return new WaitUntil(() => Planetarium.GetUniversalTime() > currentUpdateTick); // Wait for the current update tick in BDACompetitionMode so that spawning occurs after checks for dead vessels there. - yield return waitForFixedUpdate; - } - else - { - yield return new WaitForSeconds(1); // 1s between checks. Nothing much happens if nothing needs spawning. - } - } - #endregion - vesselsSpawningContinuously = false; - Debug.Log("[BDArmory.VesselSpawner]: Continuous vessel spawning ended."); - } - - // Stagger the spawn slots to avoid consecutive craft being launched too close together. - private List OptimiseSpawnSlots(int slotCount) - { - var availableSlots = Enumerable.Range(0, slotCount).ToList(); - if (slotCount < 4) return availableSlots; // Can't do anything about it for < 4 craft. - var separation = Mathf.CeilToInt(slotCount / 3f); // Start with approximately 120° separation. - var pos = 0; - var optimisedSlots = new List(); - while (optimisedSlots.Count < slotCount) - { - while (optimisedSlots.Contains(pos)) { ++pos; pos %= slotCount; } - optimisedSlots.Add(pos); - pos += separation; - pos %= slotCount; - } - return optimisedSlots; - } - - #region Scoring - // For tracking scores across multiple spawns. - public class ContinuousSpawningScores - { - public Vessel vessel; // The vessel. - public int spawnCount = 0; // The number of times a craft has been spawned. - public double outOfAmmoTime = 0; // The time the vessel ran out of ammo. - public Dictionary scoreData = new Dictionary(); - public double cumulativeTagTime = 0; - public int cumulativeHits = 0; - public int cumulativeDamagedPartsDueToRamming = 0; - public int cumulativeDamagedPartsDueToRockets = 0; - public int cumulativeDamagedPartsDueToMissiles = 0; - }; - public Dictionary continuousSpawningScores; - public void UpdateCompetitionScores(Vessel vessel, bool newSpawn = false) - { - var vesselName = vessel.vesselName; - if (!continuousSpawningScores.ContainsKey(vesselName)) return; - var spawnCount = continuousSpawningScores[vesselName].spawnCount - 1; - if (spawnCount < 0) return; // Initial spawning after scores were reset. - var scoreData = continuousSpawningScores[vesselName].scoreData; - if (BDACompetitionMode.Instance.Scores.Players.Contains(vesselName)) - { - scoreData[spawnCount] = BDACompetitionMode.Instance.Scores.ScoreData[vesselName]; // Save the Score instance for the vessel. - if (newSpawn) - { - continuousSpawningScores[vesselName].cumulativeTagTime = scoreData.Sum(kvp => kvp.Value.tagTotalTime); - continuousSpawningScores[vesselName].cumulativeHits = scoreData.Sum(kvp => kvp.Value.hits); - continuousSpawningScores[vesselName].cumulativeDamagedPartsDueToRamming = scoreData.Sum(kvp => kvp.Value.totalDamagedPartsDueToRamming); - continuousSpawningScores[vesselName].cumulativeDamagedPartsDueToRockets = scoreData.Sum(kvp => kvp.Value.totalDamagedPartsDueToRockets); - continuousSpawningScores[vesselName].cumulativeDamagedPartsDueToMissiles = scoreData.Sum(kvp => kvp.Value.totalDamagedPartsDueToMissiles); - BDACompetitionMode.Instance.Scores.RemovePlayer(vesselName); - BDACompetitionMode.Instance.Scores.AddPlayer(vessel); - BDACompetitionMode.Instance.Scores.ScoreData[vesselName].lastDamageTime = scoreData[spawnCount].lastDamageTime; - BDACompetitionMode.Instance.Scores.ScoreData[vesselName].lastPersonWhoDamagedMe = scoreData[spawnCount].lastPersonWhoDamagedMe; - } - } - } - - public void DumpContinuousSpawningScores(string tag = "") - { - var logStrings = new List(); - - if (continuousSpawningScores == null || continuousSpawningScores.Count == 0) return; - foreach (var vesselName in continuousSpawningScores.Keys) - UpdateCompetitionScores(continuousSpawningScores[vesselName].vessel); - BDACompetitionMode.Instance.competitionStatus.Add("Dumping scores for competition " + BDACompetitionMode.Instance.CompetitionID.ToString() + (tag != "" ? " " + tag : "")); - logStrings.Add("[BDArmory.VesselSpawner:" + BDACompetitionMode.Instance.CompetitionID + "]: Dumping Results at " + (int)(Planetarium.GetUniversalTime() - BDACompetitionMode.Instance.competitionStartTime) + "s"); - foreach (var vesselName in continuousSpawningScores.Keys) - { - var vesselScore = continuousSpawningScores[vesselName]; - var scoreData = vesselScore.scoreData; - logStrings.Add("[BDArmory.VesselSpawner:" + BDACompetitionMode.Instance.CompetitionID + "]: Name:" + vesselName); - logStrings.Add("[BDArmory.VesselSpawner:" + BDACompetitionMode.Instance.CompetitionID + "]: DEATHCOUNT:" + scoreData.Values.Where(v => v.deathTime >= 0).Count()); - var deathTimes = string.Join(";", scoreData.Values.Where(v => v.deathTime >= 0).Select(v => v.deathTime.ToString("0.0"))); - if (deathTimes != "") logStrings.Add("[BDArmory.VesselSpawner:" + BDACompetitionMode.Instance.CompetitionID + "]: DEATHTIMES:" + deathTimes); - #region Bullets - var whoShotMeScores = string.Join(", ", scoreData.Where(kvp => kvp.Value.hitCounts.Count > 0).Select(kvp => kvp.Key + ":" + string.Join(";", kvp.Value.hitCounts.Select(kvp2 => kvp2.Value + ":" + kvp2.Key)))); - if (whoShotMeScores != "") logStrings.Add("[BDArmory.VesselSpawner:" + BDACompetitionMode.Instance.CompetitionID + "]: WHOSHOTME:" + whoShotMeScores); - var whoDamagedMeWithBulletsScores = string.Join(", ", scoreData.Where(kvp => kvp.Value.damageFromGuns.Count > 0).Select(kvp => kvp.Key + ":" + string.Join(";", kvp.Value.damageFromGuns.Select(kvp2 => kvp2.Value.ToString("0.0") + ":" + kvp2.Key)))); - if (whoDamagedMeWithBulletsScores != "") logStrings.Add("[BDArmory.VesselSpawner:" + BDACompetitionMode.Instance.CompetitionID + "]: WHODAMAGEDMEWITHBULLETS:" + whoDamagedMeWithBulletsScores); - #endregion - #region Rockets - var whoStruckMeWithRocketsScores = string.Join(", ", scoreData.Where(kvp => kvp.Value.rocketStrikeCounts.Count > 0).Select(kvp => kvp.Key + ":" + string.Join(";", kvp.Value.rocketStrikeCounts.Select(kvp2 => kvp2.Value + ":" + kvp2.Key)))); - if (whoStruckMeWithRocketsScores != "") logStrings.Add("[BDArmory.VesselSpawner:" + BDACompetitionMode.Instance.CompetitionID + "]: WHOSTRUCKMEWITHROCKETS:" + whoStruckMeWithRocketsScores); - var whoPartsHitMeWithRocketsScores = string.Join(", ", scoreData.Where(kvp => kvp.Value.rocketPartDamageCounts.Count > 0).Select(kvp => kvp.Key + ":" + string.Join(";", kvp.Value.rocketPartDamageCounts.Select(kvp2 => kvp2.Value + ":" + kvp2.Key)))); - if (whoPartsHitMeWithRocketsScores != "") logStrings.Add("[BDArmory.VesselSpawner:" + BDACompetitionMode.Instance.CompetitionID + "]: WHOPARTSHITMEWITHROCKETS:" + whoPartsHitMeWithRocketsScores); - var whoDamagedMeWithRocketsScores = string.Join(", ", scoreData.Where(kvp => kvp.Value.damageFromRockets.Count > 0).Select(kvp => kvp.Key + ":" + string.Join(";", kvp.Value.damageFromRockets.Select(kvp2 => kvp2.Value.ToString("0.0") + ":" + kvp2.Key)))); - if (whoDamagedMeWithRocketsScores != "") logStrings.Add("[BDArmory.VesselSpawner:" + BDACompetitionMode.Instance.CompetitionID + "]: WHODAMAGEDMEWITHROCKETS:" + whoDamagedMeWithRocketsScores); - #endregion - #region Missiles - var whoStruckMeWithMissilesScores = string.Join(", ", scoreData.Where(kvp => kvp.Value.missileHitCounts.Count > 0).Select(kvp => kvp.Key + ":" + string.Join(";", kvp.Value.missileHitCounts.Select(kvp2 => kvp2.Value + ":" + kvp2.Key)))); - if (whoStruckMeWithMissilesScores != "") logStrings.Add("[BDArmory.VesselSpawner:" + BDACompetitionMode.Instance.CompetitionID + "]: WHOSTRUCKMEWITHMISSILES:" + whoStruckMeWithMissilesScores); - var whoPartsHitMeWithMissilesScores = string.Join(", ", scoreData.Where(kvp => kvp.Value.missilePartDamageCounts.Count > 0).Select(kvp => kvp.Key + ":" + string.Join(";", kvp.Value.missilePartDamageCounts.Select(kvp2 => kvp2.Value + ":" + kvp2.Key)))); - if (whoPartsHitMeWithMissilesScores != "") logStrings.Add("[BDArmory.VesselSpawner:" + BDACompetitionMode.Instance.CompetitionID + "]: WHOPARTSHITMEWITHMISSILES:" + whoPartsHitMeWithMissilesScores); - var whoDamagedMeWithMissilesScores = string.Join(", ", scoreData.Where(kvp => kvp.Value.damageFromMissiles.Count > 0).Select(kvp => kvp.Key + ":" + string.Join(";", kvp.Value.damageFromMissiles.Select(kvp2 => kvp2.Value.ToString("0.0") + ":" + kvp2.Key)))); - if (whoDamagedMeWithMissilesScores != "") logStrings.Add("[BDArmory.VesselSpawner:" + BDACompetitionMode.Instance.CompetitionID + "]: WHODAMAGEDMEWITHMISSILES:" + whoDamagedMeWithMissilesScores); - #endregion - #region Rams - var whoRammedMeScores = string.Join(", ", scoreData.Where(kvp => kvp.Value.rammingPartLossCounts.Count > 0).Select(kvp => kvp.Key + ":" + string.Join(";", kvp.Value.rammingPartLossCounts.Select(kvp2 => kvp2.Value + ":" + kvp2.Key)))); - if (whoRammedMeScores != "") logStrings.Add("[BDArmory.VesselSpawner:" + BDACompetitionMode.Instance.CompetitionID + "]: WHORAMMEDME:" + whoRammedMeScores); - #endregion - #region Kills - var GMKills = string.Join(", ", scoreData.Where(kvp => kvp.Value.gmKillReason != GMKillReason.None).Select(kvp => kvp.Key + ":" + kvp.Value.gmKillReason)); - if (GMKills != "") logStrings.Add("[BDArmory.VesselSpawner:" + BDACompetitionMode.Instance.CompetitionID + "]: GMKILL:" + GMKills); - var specialKills = new HashSet { AliveState.CleanKill, AliveState.HeadShot, AliveState.KillSteal }; // FIXME expand these to the separate special kill types - var cleanKills = string.Join(", ", scoreData.Where(kvp => specialKills.Contains(kvp.Value.aliveState) && kvp.Value.lastDamageWasFrom == DamageFrom.Guns).Select(kvp => kvp.Key + ":" + kvp.Value.lastPersonWhoDamagedMe)); - if (cleanKills != "") logStrings.Add("[BDArmory.VesselSpawner:" + BDACompetitionMode.Instance.CompetitionID + "]: CLEANKILL:" + cleanKills); - var cleanFrags = string.Join(", ", scoreData.Where(kvp => specialKills.Contains(kvp.Value.aliveState) && kvp.Value.lastDamageWasFrom == DamageFrom.Rockets).Select(kvp => kvp.Key + ":" + kvp.Value.lastPersonWhoDamagedMe)); - if (cleanFrags != "") logStrings.Add("[BDArmory.VesselSpawner:" + BDACompetitionMode.Instance.CompetitionID + "]: CLEANFRAG:" + cleanFrags); - var cleanRams = string.Join(", ", scoreData.Where(kvp => specialKills.Contains(kvp.Value.aliveState) && kvp.Value.lastDamageWasFrom == DamageFrom.Ramming).Select(kvp => kvp.Key + ":" + kvp.Value.lastPersonWhoDamagedMe)); - if (cleanRams != "") logStrings.Add("[BDArmory.VesselSpawner:" + BDACompetitionMode.Instance.CompetitionID + "]: CLEANRAM:" + cleanRams); - var cleanMissileKills = string.Join(", ", scoreData.Where(kvp => specialKills.Contains(kvp.Value.aliveState) && kvp.Value.lastDamageWasFrom == DamageFrom.Missiles).Select(kvp => kvp.Key + ":" + kvp.Value.lastPersonWhoDamagedMe)); - if (cleanMissileKills != "") logStrings.Add("[BDArmory.VesselSpawner:" + BDACompetitionMode.Instance.CompetitionID + "]: CLEANMISSILEKILL:" + cleanMissileKills); - #endregion - var accuracy = string.Join(", ", scoreData.Select(kvp => kvp.Key + ":" + kvp.Value.hits + "/" + kvp.Value.shotsFired + ":" + kvp.Value.rocketStrikes + "/" + kvp.Value.rocketsFired)); - if (accuracy != "") logStrings.Add("[BDArmory.VesselSpawner:" + BDACompetitionMode.Instance.CompetitionID + "]: ACCURACY:" + accuracy); - if (BDArmorySettings.TAG_MODE) - { - if (scoreData.Sum(kvp => kvp.Value.tagScore) > 0) logStrings.Add("[BDArmory.VesselSpawner:" + BDACompetitionMode.Instance.CompetitionID + "]: TAGSCORE:" + string.Join(", ", scoreData.Where(kvp => kvp.Value.tagScore > 0).Select(kvp => kvp.Key + ":" + kvp.Value.tagScore.ToString("0.0")))); - if (scoreData.Sum(kvp => kvp.Value.tagTotalTime) > 0) logStrings.Add("[BDArmory.VesselSpawner:" + BDACompetitionMode.Instance.CompetitionID + "]: TIMEIT:" + string.Join(", ", scoreData.Where(kvp => kvp.Value.tagTotalTime > 0).Select(kvp => kvp.Key + ":" + kvp.Value.tagTotalTime.ToString("0.0")))); - if (scoreData.Sum(kvp => kvp.Value.tagKillsWhileIt) > 0) logStrings.Add("[BDArmory.VesselSpawner:" + BDACompetitionMode.Instance.CompetitionID + "]: KILLSWHILEIT:" + string.Join(", ", scoreData.Where(kvp => kvp.Value.tagKillsWhileIt > 0).Select(kvp => kvp.Key + ":" + kvp.Value.tagKillsWhileIt))); - if (scoreData.Sum(kvp => kvp.Value.tagTimesIt) > 0) logStrings.Add("[BDArmory.VesselSpawner:" + BDACompetitionMode.Instance.CompetitionID + "]: TIMESIT:" + string.Join(", ", scoreData.Where(kvp => kvp.Value.tagTimesIt > 0).Select(kvp => kvp.Key + ":" + kvp.Value.tagTimesIt))); - } - } - - // Dump the log results to a file. - if (BDACompetitionMode.Instance.CompetitionID > 0) - { - var folder = Path.Combine(KSPUtil.ApplicationRootPath, "GameData", "BDArmory", "Logs"); - if (!Directory.Exists(folder)) - Directory.CreateDirectory(folder); - File.WriteAllLines(Path.Combine(folder, BDACompetitionMode.Instance.CompetitionID.ToString() + (tag != "" ? "-" + tag : "") + ".log"), logStrings); - } - } - #endregion - - public void KillOffOutOfAmmoVessels() - { - if (BDArmorySettings.OUT_OF_AMMO_KILL_TIME < 0) return; // Never - var now = Planetarium.GetUniversalTime(); - Vessel vessel; - MissileFire weaponManager; - ContinuousSpawningScores score; - foreach (var vesselName in continuousSpawningScores.Keys) - { - score = continuousSpawningScores[vesselName]; - vessel = score.vessel; - if (vessel == null) continue; // Vessel hasn't been respawned yet. - weaponManager = VesselModuleRegistry.GetModule(vessel); - if (weaponManager == null) continue; // Weapon manager hasn't registered yet. - if (score.outOfAmmoTime == 0 && !weaponManager.HasWeaponsAndAmmo()) - score.outOfAmmoTime = Planetarium.GetUniversalTime(); - if (score.outOfAmmoTime > 0 && now - score.outOfAmmoTime > BDArmorySettings.OUT_OF_AMMO_KILL_TIME) - { - var m = "Killing off " + vesselName + " as they exceeded the out-of-ammo kill time."; - BDACompetitionMode.Instance.competitionStatus.Add(m); - Debug.Log("[BDArmory.VesselSpawner]: " + m); - BDACompetitionMode.Instance.Scores.RegisterDeath(vesselName, GMKillReason.OutOfAmmo); // Indicate that it was us who killed it and remove any "clean" kills. - SpawnUtils.RemoveVessel(vessel); - } - } - } - } -} \ No newline at end of file diff --git a/BDArmory/Competition/VesselSpawning/SingleVesselSpawning.cs b/BDArmory/Competition/VesselSpawning/SingleVesselSpawning.cs deleted file mode 100644 index fd7502bb6..000000000 --- a/BDArmory/Competition/VesselSpawning/SingleVesselSpawning.cs +++ /dev/null @@ -1,193 +0,0 @@ -using UnityEngine; -using System.Collections; -using System.Collections.Generic; -using System.Linq; - -using BDArmory.Modules; -using BDArmory.UI; - -namespace BDArmory.Competition.VesselSpawning -{ - [KSPAddon(KSPAddon.Startup.Flight, false)] - public class SingleVesselSpawning : VesselSpawner - { - public static SingleVesselSpawning Instance; - - void Awake() - { - if (Instance) - Destroy(Instance); - Instance = this; - } - - // AUBRANIUM Most of this class is really more suitable as a suplement to VesselLoader to be used as a work-horse for the individual spawning of the vessels in the other VesselSpawner classes. - // However, it also needs to be a VesselSpawner derived class in its own right for use in the PointSpawnStrategy. - public override IEnumerator Spawn(SpawnConfig spawnConfig) => SpawnVessel(spawnConfig.craftFiles.First(), spawnConfig.latitude, spawnConfig.longitude, spawnConfig.altitude); // AUBRANIUM, this is a kludge to get the VesselSpawner class to work with the SpawnStrategy interface. - - public List spawnedVessels = new List(); - // TODO DOCNAPPERS - // AUBNRANIUM, I haven't managed to check this function properly yet, but here are some notes in any case. - // This will need to call most of what's in the main part of the continuous spawning coroutine (lines 1298—1524). (I think this is up-to-date with the current state of the regular single spawning, but there may be one or two edge cases.) - // Additionally, if ground spawning is also desired, then a fair bit of what's in the "Post-spawning" region of SpawnAllVesselsOnceCoroutine will also be needed. - // Why -0.7f pitch? Also, angles are measured in degrees for the most part in Unity in case this is meant to be -0.7rad. - public IEnumerator SpawnVessel(string craftUrl, double latitude, double longitude, double altitude, float initialHeading = 0.0f, float initialPitch = -0.7f) - { - // AUBRANIUM, this shouldn't use vesselsSpawning as a guard as it's desirable to be able to spawn multiple vessels this way concurrently in a similar manner to how RemoveVessel works. - if (vesselsSpawning) - { - Debug.Log("[BDArmory.VesselSpawner]: Already spawning craft"); - yield break; - } - spawnedVessels.Clear(); - vesselsSpawning = true; - vesselSpawnSuccess = false; - var terrainAltitude = FlightGlobals.currentMainBody.TerrainAltitude(latitude, longitude); - var spawnPoint = FlightGlobals.currentMainBody.GetWorldSurfacePosition(latitude, longitude, terrainAltitude + altitude); - Vector3d craftGeoCoords; - var shipFacility = EditorFacility.None; - FlightGlobals.currentMainBody.GetLatLonAlt(spawnPoint, out craftGeoCoords.x, out craftGeoCoords.y, out craftGeoCoords.z); // Convert spawn point to geo-coords for the actual spawning function. - Vessel vessel = null; - try - { - vessel = VesselLoader.SpawnVesselFromCraftFile(craftUrl, craftGeoCoords, initialHeading, initialPitch, 0f, out shipFacility); // SPAWN - } - catch { vessel = null; } - if (vessel == null) - { - var location = string.Format("({0:##.###}, {1:##.###}, {2:####}", latitude, longitude, altitude); - Debug.Log("[BDArmory.VesselSpawner]: Failed to spawn craft " + craftUrl + " " + location); - vesselsSpawning = false; - yield break; - } - vessel.Landed = false; - vessel.ResumeStaging(); - - yield return waitForFixedUpdate; - yield return waitForFixedUpdate; - yield return waitForFixedUpdate; - - // wait for loaded vessel switcher - var result = new List { vessel }; - yield return WaitForLoadedVesselSwitcher(result); - if (!loadedVesselResult || waitingForLoadedVesselSwitcher) - { - Debug.Log("[BDArmory.VesselSpawner]: Timeout waiting for weapon managers"); - vesselsSpawning = false; - yield break; - } - - // check for AI - yield return CheckForAI(result); - if (!checkResultAI || waitingForAICheck) - { - Debug.Log("[BDArmory.VesselSpawner]: Timeout waiting for AIPilot"); - vesselsSpawning = false; - yield break; - } - - Debug.Log(string.Format("[BDArmory.VesselSpawner]: Spawned {0} craft successfully!", result.Count)); - spawnedVessels = result; - vesselsSpawning = false; - vesselSpawnSuccess = true; - } - - - // TODO DOCNAPPERS: Check whether the following performs the correct procedures that are done in SpawnAllVesselsOnceCoroutine and SpawnVesselsContinuouslyCoroutine as mentioned in the SpawnVessel coroutine above. - private bool waitingForLoadedVesselSwitcher = false; - private bool loadedVesselResult = false; - private IEnumerator WaitForLoadedVesselSwitcher(List vessels) - { - waitingForLoadedVesselSwitcher = true; - loadedVesselResult = false; - var timeoutAt = Planetarium.GetUniversalTime() + 10.0; - bool timeoutElapsed = false; - var remainingVessels = vessels.ToList(); - var checkedVessels = new List(); - - while (!timeoutElapsed) - { - if (!remainingVessels.Any()) - { - break; - } - LoadedVesselSwitcher.Instance.UpdateList(); - var weaponManagers = LoadedVesselSwitcher.Instance.WeaponManagers.SelectMany(tm => tm.Value).ToList(); - - foreach (var vessel in remainingVessels) - { - if (!vessel.loaded || vessel.packed) continue; - VesselModuleRegistry.OnVesselModified(vessel, true); - var weaponManager = VesselModuleRegistry.GetModule(vessel); - if (weaponManager != null && weaponManagers.Contains(weaponManager)) // The weapon manager has been added, let's go! - { - checkedVessels.Add(vessel); - vessel.ActionGroups.ToggleGroup(BDACompetitionMode.KM_dictAG[10]); // Modular Missiles use lower AGs (1-3) for staging, use a high AG number to not affect them - if (weaponManager.guardMode) weaponManager.ToggleGuardMode(); // Disable guard mode (in case someone enabled it on AG10). - var engines = VesselModuleRegistry.GetModules(vessel); - if (!engines.Any(engine => engine.EngineIgnited)) // If the vessel didn't activate their engines on AG10, then activate all their engines and hope for the best. - { - engines.ForEach(e => e.Activate()); - } - } - } - checkedVessels.ForEach(e => remainingVessels.Remove(e)); - yield return waitForFixedUpdate; - timeoutElapsed = Planetarium.GetUniversalTime() > timeoutAt; - } - - loadedVesselResult = !timeoutElapsed; - waitingForLoadedVesselSwitcher = false; - } - - private bool checkResultAI = false; - private bool waitingForAICheck = false; - - private IEnumerator CheckForAI(List vessels) - { - Debug.Log("[BDArmory.VesselSpawner]: Checking for AIPilot"); - waitingForAICheck = true; - checkResultAI = false; - bool timeoutElapsed = false; - var remainingVessels = vessels.ToList(); - var checkedVessels = new List(); - var timeoutAt = Planetarium.GetUniversalTime() + 10.0; - - - while (!checkResultAI && !timeoutElapsed) - { - // iterate through vessels and check for pilot module on each - if (remainingVessels.Any()) - { - foreach (var vessel in remainingVessels) - { - if (!vessel.loaded) - { - Debug.Log("[BDArmory.VesselSpawner] Vessel not loaded"); - continue; - } - if (vessel.packed) - { - Debug.Log("[BDArmory.VesselSpawner] Vessel packed"); - continue; - } - var vesselPilot = VesselModuleRegistry.GetBDModulePilotAI(vessel, true); - if (vesselPilot != null) - { - vesselPilot.ActivatePilot(); - checkedVessels.Add(vessel); - } - } - checkedVessels.ForEach(e => remainingVessels.Remove(e)); - } - else - { - checkResultAI = true; - } - yield return waitForFixedUpdate; - timeoutElapsed = Planetarium.GetUniversalTime() > timeoutAt; - } - - waitingForAICheck = false; - } - } -} \ No newline at end of file diff --git a/BDArmory/Competition/VesselSpawning/SpawnConfig.cs b/BDArmory/Competition/VesselSpawning/SpawnConfig.cs deleted file mode 100644 index 04e2bb421..000000000 --- a/BDArmory/Competition/VesselSpawning/SpawnConfig.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System; -using System.Linq; -using System.Collections.Generic; - -namespace BDArmory.Competition.VesselSpawning -{ - /// - /// Configuration for spawning groups of vessels. - /// - /// Note: - /// This is currently partially specific to SpawnAllVesselsOnce and SpawnVesselsContinuosly. - /// Once the spawn strategies take over that functionality, those components may be dropped from here. - /// - [Serializable] - public class SpawnConfig - { - public SpawnConfig(int worldIndex, double latitude, double longitude, double altitude, float distance, bool absDistanceOrFactor, float easeInSpeed = 1f, bool killEverythingFirst = true, bool assignTeams = true, int numberOfTeams = 0, List teamCounts = null, List> teamsSpecific = null, string folder = "", List craftFiles = null) - { - this.worldIndex = worldIndex; - this.latitude = latitude; - this.longitude = longitude; - this.altitude = altitude; - this.distance = distance; - this.absDistanceOrFactor = absDistanceOrFactor; - this.easeInSpeed = easeInSpeed; - this.killEverythingFirst = killEverythingFirst; - this.assignTeams = assignTeams; - this.numberOfTeams = numberOfTeams; - this.teamCounts = teamCounts; if (teamCounts != null) this.numberOfTeams = this.teamCounts.Count; - this.teamsSpecific = teamsSpecific; - this.folder = folder ?? ""; - this.craftFiles = craftFiles; - } - public SpawnConfig(SpawnConfig other) - { - this.worldIndex = other.worldIndex; - this.latitude = other.latitude; - this.longitude = other.longitude; - this.altitude = other.altitude; - this.distance = other.distance; - this.absDistanceOrFactor = other.absDistanceOrFactor; - this.easeInSpeed = other.easeInSpeed; - this.killEverythingFirst = other.killEverythingFirst; - this.assignTeams = other.assignTeams; - this.numberOfTeams = other.numberOfTeams; - this.teamCounts = other.teamCounts; - this.teamsSpecific = other.teamsSpecific; - this.folder = other.folder; - this.craftFiles = other.craftFiles?.ToList(); - } - public int worldIndex; - public double latitude; - public double longitude; - public double altitude; - public float distance; - public bool absDistanceOrFactor; // If true, the distance value is used as-is, otherwise it is used as a factor giving the actual distance: (N+1)*distance, where N is the number of vessels. - public float easeInSpeed; - public bool killEverythingFirst = true; - public bool assignTeams = true; - public int numberOfTeams = 0; // Number of teams (or FFA, Folders or Inf). For evenly (as possible) splitting vessels into teams. - public List teamCounts; // List of team numbers. For unevenly splitting vessels into teams based on their order in the tournament state file for the round. E.g., when spawning from folders. - public List> teamsSpecific; // Dictionary of vessels and teams. For splitting specific vessels into specific teams. - public string folder = ""; - public List craftFiles = null; - } -} \ No newline at end of file diff --git a/BDArmory/Competition/VesselSpawning/SpawnUtils.cs b/BDArmory/Competition/VesselSpawning/SpawnUtils.cs deleted file mode 100644 index 1dee2b3e6..000000000 --- a/BDArmory/Competition/VesselSpawning/SpawnUtils.cs +++ /dev/null @@ -1,441 +0,0 @@ -using UnityEngine; -using System; -using System.IO; -using System.Collections; -using System.Collections.Generic; -using System.Linq; - -using BDArmory.Core; -using BDArmory.Core.Utils; -using BDArmory.GameModes; -using BDArmory.Modules; -using BDArmory.UI; -using BDArmory.Misc; - -namespace BDArmory.Competition.VesselSpawning -{ - public enum SpawnFailureReason { None, NoCraft, NoTerrain, InvalidVessel, VesselLostParts, VesselFailedToSpawn, TimedOut }; - - public static class SpawnUtils - { - private static string _spawnProbeLocation = null; - public static string spawnProbeLocation - { - get - { - if (_spawnProbeLocation != null) return _spawnProbeLocation; - _spawnProbeLocation = Path.Combine(KSPUtil.ApplicationRootPath, "GameData", "BDArmory", "craft", "SpawnProbe.craft"); // SpaceDock location - if (!File.Exists(_spawnProbeLocation)) _spawnProbeLocation = Path.Combine(KSPUtil.ApplicationRootPath, "Ships", "SPH", "SpawnProbe.craft"); // CKAN location - if (!File.Exists(_spawnProbeLocation)) - { - _spawnProbeLocation = null; - var message = "SpawnProbe.craft is missing. Your installation is likely corrupt."; - BDACompetitionMode.Instance.competitionStatus.Add(message); - Debug.LogError("[BDArmory.SpawnUtils]: " + message); - } - return _spawnProbeLocation; - } - } - - public static Vessel SpawnSpawnProbe() - { - // Spawn in the SpawnProbe at the camera position and switch to it so that we can clean up the other vessels properly. - var dummyVar = EditorFacility.None; - Vector3d dummySpawnCoords; - FlightGlobals.currentMainBody.GetLatLonAlt(FlightCamera.fetch.transform.position, out dummySpawnCoords.x, out dummySpawnCoords.y, out dummySpawnCoords.z); - if (spawnProbeLocation == null) return null; - Vessel spawnProbe = VesselLoader.SpawnVesselFromCraftFile(spawnProbeLocation, dummySpawnCoords, 0f, 0f, 0f, out dummyVar); - return spawnProbe; - } - - // Cancel all spawning modes. - public static void CancelSpawning() - { - // Single spawn - if (CircularSpawning.Instance.vesselsSpawning) - { CircularSpawning.Instance.CancelSpawning(); } - - // Continuous spawn - if (ContinuousSpawning.Instance.vesselsSpawningContinuously) - { ContinuousSpawning.Instance.CancelSpawning(); } - - SpawnUtils.RevertSpawnLocationCamera(true); - } - - #region Camera - public static void ShowSpawnPoint(int worldIndex, double latitude, double longitude, double altitude = 0, float distance = 100, bool spawning = false, bool recurse = true) => SpawnUtilsInstance.Instance.ShowSpawnPoint(worldIndex, latitude, longitude, altitude, distance, spawning, recurse); - public static void RevertSpawnLocationCamera(bool keepTransformValues = true) => SpawnUtilsInstance.Instance.RevertSpawnLocationCamera(keepTransformValues); - #endregion - - #region Teams - public static Dictionary originalTeams = new Dictionary(); - public static void SaveTeams() - { - originalTeams.Clear(); - foreach (var weaponManager in LoadedVesselSwitcher.Instance.WeaponManagers.SelectMany(tm => tm.Value).ToList()) - { - originalTeams[weaponManager.vessel.vesselName] = weaponManager.Team.Name; - } - } - #endregion - - #region Engine Activation - public static int CountActiveEngines(Vessel vessel) - { - return VesselModuleRegistry.GetModules(vessel).Where(engine => engine.EngineIgnited).ToList().Count + FireSpitter.CountActiveEngines(vessel); - } - - public static void ActivateAllEngines(Vessel vessel, bool activate = true) - { - foreach (var engine in VesselModuleRegistry.GetModules(vessel)) - { - var mme = engine.part.FindModuleImplementing(); - if (mme == null) - { - if (activate) engine.Activate(); - else engine.Shutdown(); - } - else - { - if (mme.runningPrimary) - { - if (activate && !mme.PrimaryEngine.EngineIgnited) - { - mme.PrimaryEngine.Activate(); - } - else if (!activate && mme.PrimaryEngine.EngineIgnited) - { - mme.PrimaryEngine.Shutdown(); - } - } - else - { - if (activate && !mme.SecondaryEngine.EngineIgnited) - { - mme.SecondaryEngine.Activate(); - } - else if (!activate && mme.SecondaryEngine.EngineIgnited) - { - mme.SecondaryEngine.Shutdown(); - } - } - } - } - FireSpitter.ActivateFSEngines(vessel, activate); - } - #endregion - - #region Intake hacks - public static void HackIntakesOnNewVessels(bool enable) - { - if (enable) - { - GameEvents.onVesselLoaded.Add(HackIntakesEventHandler); - GameEvents.OnVesselRollout.Add(HackIntakes); - } - else - { - GameEvents.onVesselLoaded.Remove(HackIntakesEventHandler); - GameEvents.OnVesselRollout.Remove(HackIntakes); - } - } - static void HackIntakesEventHandler(Vessel vessel) => HackIntakes(vessel, true); - - public static void HackIntakes(Vessel vessel, bool enable) - { - if (vessel == null || !vessel.loaded) return; - if (enable) - { - foreach (var intake in VesselModuleRegistry.GetModules(vessel)) - intake.checkForOxygen = false; - } - else - { - foreach (var intake in VesselModuleRegistry.GetModules(vessel)) - { - var checkForOxygen = ConfigNodeUtils.FindPartModuleConfigNodeValue(intake.part.partInfo.partConfig, "ModuleResourceIntake", "checkForOxygen"); - if (!string.IsNullOrEmpty(checkForOxygen)) // Use the default value from the part. - { - try - { - intake.checkForOxygen = bool.Parse(checkForOxygen); - } - catch (Exception e) - { - Debug.LogError($"[BDArmory.BDArmorySetup]: Failed to parse checkForOxygen configNode of {intake.name}: {e.Message}"); - } - } - else - { - Debug.LogWarning($"[BDArmory.BDArmorySetup]: No default value for checkForOxygen found in partConfig for {intake.name}, defaulting to true."); - intake.checkForOxygen = true; - } - } - } - } - public static void HackIntakes(ShipConstruct ship) // This version only needs to enable the hack. - { - if (ship == null) return; - foreach (var part in ship.Parts) - { - var intakes = part.FindModulesImplementing(); - if (intakes.Count() > 0) - { - foreach (var intake in intakes) - intake.checkForOxygen = false; - } - } - } - #endregion - - #region Vessel Removal - public static bool removingVessels => SpawnUtilsInstance.Instance.removeVesselsPending > 0; - public static void RemoveVessel(Vessel vessel) => SpawnUtilsInstance.Instance.RemoveVessel(vessel); - public static IEnumerator RemoveAllVessels() => SpawnUtilsInstance.Instance.RemoveAllVessels(); - #endregion - - #region AI/WM stuff for RWP - public static bool CheckAIWMPlacement(Vessel vessel) - { - var message = ""; - List failureStrings = new List(); - var AI = VesselModuleRegistry.GetBDModulePilotAI(vessel, true); - var WM = VesselModuleRegistry.GetMissileFire(vessel, true); - if (AI == null) message = " has no AI"; - if (WM == null) message += (AI == null ? " or WM" : " has no WM"); - if (AI != null || WM != null) - { - int count = 0; - if (AI != null && (AI.part.parent == null || AI.part.vessel.rootPart != AI.part.parent)) - { - message += (WM == null ? " and its AI" : "'s AI"); - ++count; - } - if (WM != null && (WM.part.parent == null || WM.part.vessel.rootPart != WM.part.parent)) - { - message += (AI == null ? " and its WM" : (count > 0 ? " and WM" : "'s WM")); - ++count; - }; - if (count > 0) message += (count > 1 ? " are" : " is") + " not attached to its root part"; - } - - if (!string.IsNullOrEmpty(message)) - { - message = $"{vessel.vesselName}" + message + "."; - BDACompetitionMode.Instance.competitionStatus.Add(message); - Debug.LogWarning("[BDArmory.SpawnUtils]: " + message); - return false; - } - return true; - } - - public static void CheckAIWMCounts(Vessel vessel) - { - var numberOfAIs = VesselModuleRegistry.GetModuleCount(vessel); - var numberOfWMs = VesselModuleRegistry.GetModuleCount(vessel); - string message = null; - if (numberOfAIs != 1 && numberOfWMs != 1) message = $"{vessel.vesselName} has {numberOfAIs} AIs and {numberOfWMs} WMs"; - else if (numberOfAIs != 1) message = $"{vessel.vesselName} has {numberOfAIs} AIs"; - else if (numberOfWMs != 1) message = $"{vessel.vesselName} has {numberOfWMs} WMs"; - if (message != null) - { - BDACompetitionMode.Instance.competitionStatus.Add(message); - Debug.LogWarning("[BDArmory.SpawnUtils]: " + message); - } - } - #endregion - - } - - /// - /// Non-static MonoBehaviour version to be able to call coroutines. - /// - [KSPAddon(KSPAddon.Startup.Flight, false)] - public class SpawnUtilsInstance : MonoBehaviour - { - public static SpawnUtilsInstance Instance; - - void Awake() - { - if (Instance != null) Destroy(Instance); - Instance = this; - spawnLocationCamera = new GameObject("StationaryCameraParent"); - spawnLocationCamera = (GameObject)Instantiate(spawnLocationCamera, Vector3.zero, Quaternion.identity); - spawnLocationCamera.SetActive(false); - if (BDArmorySettings.HACK_INTAKES) SpawnUtils.HackIntakesOnNewVessels(true); - } - - void OnDestroy() - { - VesselSpawnerField.Save(); - Destroy(spawnLocationCamera); - SpawnUtils.HackIntakesOnNewVessels(false); - } - - #region Vessel Removal - public int removeVesselsPending = 0; - // Remove a vessel and clean up any remaining parts. This fixes the case where the currently focussed vessel refuses to die properly. - public void RemoveVessel(Vessel vessel) - { - if (vessel == null) return; - if (BDArmorySettings.ASTEROID_RAIN && vessel.vesselType == VesselType.SpaceObject) return; // Don't remove asteroids we're using. - if (BDArmorySettings.ASTEROID_FIELD && vessel.vesselType == VesselType.SpaceObject) return; // Don't remove asteroids we're using. - StartCoroutine(RemoveVesselCoroutine(vessel)); - } - public IEnumerator RemoveVesselCoroutine(Vessel vessel) - { - if (vessel == null) yield break; - ++removeVesselsPending; - if (vessel != FlightGlobals.ActiveVessel && vessel.vesselType != VesselType.SpaceObject) - { - if (KerbalSafetyManager.Instance.safetyLevel != KerbalSafetyLevel.Off) - KerbalSafetyManager.Instance.RecoverVesselNow(vessel); - else - ShipConstruction.RecoverVesselFromFlight(vessel.protoVessel, HighLogic.CurrentGame.flightState, true); - } - else - { - if (vessel.vesselType == VesselType.SpaceObject) - { - if (BDArmorySettings.ASTEROID_RAIN && AsteroidRain.IsManagedAsteroid(vessel)) yield break; // Don't remove asteroids when we're using them. - if (BDArmorySettings.ASTEROID_FIELD && AsteroidField.IsManagedAsteroid(vessel)) yield break; // Don't remove asteroids when we're using them. - var cometVessel = vessel.FindVesselModuleImplementing(); - if (cometVessel) { Destroy(cometVessel); } - } - vessel.Die(); // Kill the vessel - yield return waitForFixedUpdate; - if (vessel != null) - { - var partsToKill = vessel.parts.ToList(); // If it left any parts, kill them. (This occurs when the currently focussed vessel gets killed.) - foreach (var part in partsToKill) - part.Die(); - } - yield return waitForFixedUpdate; - } - --removeVesselsPending; - } - - public IEnumerator RemoveAllVessels() - { - var vesselsToKill = FlightGlobals.Vessels.ToList(); - // Kill all other vessels (including debris). - foreach (var vessel in vesselsToKill) - { - RemoveVessel(vessel); - } - yield return new WaitWhile(() => removeVesselsPending > 0); - } - #endregion - - #region Camera Adjustment - GameObject spawnLocationCamera; - Transform originalCameraParentTransform; - float originalCameraNearClipPlane; - Coroutine delayedShowSpawnPointCoroutine; - private readonly WaitForFixedUpdate waitForFixedUpdate = new WaitForFixedUpdate(); - /// - /// Show the given location. - /// - /// Note: if spawning is true, then the spawnLocationCamera takes over the camera and RevertSpawnLocationCamera should be called at some point to allow KSP to do its own camera stuff. - /// - /// - /// - /// - /// - /// - /// - /// - public void ShowSpawnPoint(int worldIndex, double latitude, double longitude, double altitude = 0, float distance = 100, bool spawning = false, bool recurse = true) - { - if (BDArmorySettings.ASTEROID_RAIN) { AsteroidRain.Instance.Reset(); } - if (BDArmorySettings.ASTEROID_FIELD) { AsteroidField.Instance.Reset(); } - if (!spawning && (FlightGlobals.ActiveVessel == null || FlightGlobals.ActiveVessel.state == Vessel.State.DEAD)) - { - if (!recurse) - { - Debug.LogWarning($"[BDArmory.SpawnUtils]: No active vessel, unable to show spawn point."); - return; - } - Debug.LogWarning($"[BDArmory.SpawnUtils]: Active vessel is dead or packed, spawning a new one."); - if (delayedShowSpawnPointCoroutine != null) { StopCoroutine(delayedShowSpawnPointCoroutine); delayedShowSpawnPointCoroutine = null; } - delayedShowSpawnPointCoroutine = StartCoroutine(DelayedShowSpawnPoint(worldIndex, latitude, longitude, altitude, distance, spawning)); - return; - } - if (!spawning) - { - var overLand = (worldIndex != -1 ? FlightGlobals.Bodies[worldIndex] : FlightGlobals.currentMainBody).TerrainAltitude(latitude, longitude) > 0; - FlightGlobals.fetch.SetVesselPosition(worldIndex != -1 ? worldIndex : FlightGlobals.currentMainBody.flightGlobalsIndex, latitude, longitude, overLand ? Math.Max(5, altitude) : altitude, FlightGlobals.ActiveVessel.vesselType == VesselType.Plane ? 0 : 90, 0, true, overLand); // FIXME This should be using the vessel reference transform to determine the inclination. Also below. - var flightCamera = FlightCamera.fetch; - flightCamera.SetDistance(distance); - var radialUnitVector = (flightCamera.transform.parent.position - FlightGlobals.currentMainBody.transform.position).normalized; - flightCamera.transform.parent.rotation = Quaternion.LookRotation(flightCamera.transform.parent.forward, radialUnitVector); - VehiclePhysics.Gravity.Refresh(); - } - else - { - FlightGlobals.fetch.SetVesselPosition(worldIndex != -1 ? worldIndex : FlightGlobals.currentMainBody.flightGlobalsIndex, latitude, longitude, altitude, 0, 0, true); - var terrainAltitude = FlightGlobals.currentMainBody.TerrainAltitude(latitude, longitude); - var spawnPoint = FlightGlobals.currentMainBody.GetWorldSurfacePosition(latitude, longitude, terrainAltitude + altitude); - var radialUnitVector = (spawnPoint - FlightGlobals.currentMainBody.transform.position).normalized; - var refDirection = Math.Abs(Vector3.Dot(Vector3.up, radialUnitVector)) < 0.71f ? Vector3.up : Vector3.forward; // Avoid that the reference direction is colinear with the local surface normal. - var flightCamera = FlightCamera.fetch; - var cameraPosition = Vector3.RotateTowards(distance * radialUnitVector, Vector3.Cross(radialUnitVector, refDirection), 70f * Mathf.Deg2Rad, 0); - if (!spawnLocationCamera.activeSelf) - { - spawnLocationCamera.SetActive(true); - originalCameraParentTransform = flightCamera.transform.parent; - originalCameraNearClipPlane = BDGUIUtils.GetMainCamera().nearClipPlane; - } - spawnLocationCamera.transform.position = spawnPoint; - spawnLocationCamera.transform.rotation = Quaternion.LookRotation(-cameraPosition, radialUnitVector); - flightCamera.transform.parent = spawnLocationCamera.transform; - flightCamera.SetTarget(spawnLocationCamera.transform); - flightCamera.transform.localPosition = cameraPosition; - flightCamera.transform.localRotation = Quaternion.identity; - flightCamera.SetDistance(distance); - } - } - - IEnumerator DelayedShowSpawnPoint(int worldIndex, double latitude, double longitude, double altitude = 0, float distance = 100, bool spawning = false) - { - var dummyVar = EditorFacility.None; - Vector3d dummySpawnCoords; - FlightGlobals.currentMainBody.GetLatLonAlt(FlightCamera.fetch.transform.position + 1000f * (FlightCamera.fetch.transform.position - FlightGlobals.currentMainBody.transform.position).normalized, out dummySpawnCoords.x, out dummySpawnCoords.y, out dummySpawnCoords.z); - Vessel spawnProbe = VesselLoader.SpawnVesselFromCraftFile(SpawnUtils.spawnProbeLocation, dummySpawnCoords, 0f, 0f, 0f, out dummyVar); - spawnProbe.Landed = false; - // spawnProbe.situation = Vessel.Situations.FLYING; - // spawnProbe.IgnoreGForces(240); - yield return new WaitWhile(() => spawnProbe != null && (!spawnProbe.loaded || spawnProbe.packed)); - FlightGlobals.ForceSetActiveVessel(spawnProbe); - while (spawnProbe != null && FlightGlobals.ActiveVessel != spawnProbe) - { - spawnProbe.SetWorldVelocity(Vector3d.zero); - LoadedVesselSwitcher.Instance.ForceSwitchVessel(spawnProbe); - yield return waitForFixedUpdate; - } - ShowSpawnPoint(worldIndex, latitude, longitude, altitude, distance, spawning, false); - } - - public void RevertSpawnLocationCamera(bool keepTransformValues = true) - { - if (!spawnLocationCamera.activeSelf) return; - if (delayedShowSpawnPointCoroutine != null) { StopCoroutine(delayedShowSpawnPointCoroutine); delayedShowSpawnPointCoroutine = null; } - var flightCamera = FlightCamera.fetch; - if (originalCameraParentTransform != null) - { - if (keepTransformValues && flightCamera.transform != null && flightCamera.transform.parent != null) - { - originalCameraParentTransform.position = flightCamera.transform.parent.position; - originalCameraParentTransform.rotation = flightCamera.transform.parent.rotation; - originalCameraNearClipPlane = BDGUIUtils.GetMainCamera().nearClipPlane; - } - flightCamera.transform.parent = originalCameraParentTransform; - BDGUIUtils.GetMainCamera().nearClipPlane = originalCameraNearClipPlane; - } - if (FlightGlobals.ActiveVessel != null && FlightGlobals.ActiveVessel.state != Vessel.State.DEAD) - LoadedVesselSwitcher.Instance.ForceSwitchVessel(FlightGlobals.ActiveVessel); // Update the camera. - spawnLocationCamera.SetActive(false); - } - #endregion - } -} \ No newline at end of file diff --git a/BDArmory/Competition/VesselSpawning/VesselSpawner.cs b/BDArmory/Competition/VesselSpawning/VesselSpawner.cs deleted file mode 100644 index bc295f452..000000000 --- a/BDArmory/Competition/VesselSpawning/VesselSpawner.cs +++ /dev/null @@ -1,40 +0,0 @@ -using UnityEngine; -using System.Collections; - -using BDArmory.Core; - -namespace BDArmory.Competition.VesselSpawning -{ - /// Base class for VesselSpawner classes so that it can work with spawn strategies. - public abstract class VesselSpawner : MonoBehaviour - { - public abstract IEnumerator Spawn(SpawnConfig spawnConfig); // AUBRANIUM, this is essentially a kludge to get the VesselSpawner class to be functional with the way that the SpawnStrategy interface is defined. - - public virtual void PreSpawnInitialisation(SpawnConfig spawnConfig) - { - //Reset gravity - if (BDArmorySettings.GRAVITY_HACKS) - { - PhysicsGlobals.GraviticForceMultiplier = 1d; - VehiclePhysics.Gravity.Refresh(); - } - - // If we're on another planetary body, first switch to the proper one. - if (spawnConfig.worldIndex != FlightGlobals.currentMainBody.flightGlobalsIndex) - { SpawnUtils.ShowSpawnPoint(spawnConfig.worldIndex, spawnConfig.latitude, spawnConfig.longitude, spawnConfig.altitude, 20); } - } - - public bool vesselsSpawning { get { return VesselSpawnerStatus.vesselsSpawning; } set { VesselSpawnerStatus.vesselsSpawning = value; } } - public bool vesselSpawnSuccess { get { return VesselSpawnerStatus.vesselSpawnSuccess; } set { VesselSpawnerStatus.vesselSpawnSuccess = value; } } - public SpawnFailureReason spawnFailureReason { get { return VesselSpawnerStatus.spawnFailureReason; } set { VesselSpawnerStatus.spawnFailureReason = value; } } - protected static readonly WaitForFixedUpdate waitForFixedUpdate = new WaitForFixedUpdate(); - } - - public static class VesselSpawnerStatus - { - public static bool vesselsSpawning = false; // Flag for when vessels are being spawned and other things should wait for them to finish being spawned. - public static bool vesselSpawnSuccess = false; // Flag for whether vessel spawning was successful or not. - public static SpawnFailureReason spawnFailureReason = SpawnFailureReason.None; - public static bool inhibitCameraTools => vesselsSpawning; // Flag for CameraTools (currently just checks for vessels being spawned). - } -} \ No newline at end of file diff --git a/BDArmory/Control/BDAirspeedControl.cs b/BDArmory/Control/BDAirspeedControl.cs index 6e3cfb2d5..d0180fd43 100644 --- a/BDArmory/Control/BDAirspeedControl.cs +++ b/BDArmory/Control/BDAirspeedControl.cs @@ -1,6 +1,8 @@ using System.Collections.Generic; using UnityEngine; -using BDArmory.Modules; + +using BDArmory.Extensions; +using BDArmory.Utils; namespace BDArmory.Control { @@ -14,6 +16,7 @@ public class BDAirspeedControl : MonoBehaviour //: PartModule public bool allowAfterburner = true; public bool forceAfterburner = false; public float afterburnerPriority = 50f; + public bool forceAfterburnerIfMaxThrottle = false; //[KSPField(isPersistant = false, guiActive = true, guiActiveEditor = false, guiName = "ThrottleFactor"), // UI_FloatRange(minValue = 1f, maxValue = 20f, stepIncrement = .5f, scene = UI_Scene.All)] @@ -21,15 +24,26 @@ public class BDAirspeedControl : MonoBehaviour //: PartModule public Vessel vessel; + AxisGroupsModule axisGroupsModule; + bool hasAxisGroupsModule = false; // To avoid repeated null checks + bool controlEnabled; private float smoothedAccel = 0; // smoothed acceleration, prevents super fast toggling of afterburner + bool shouldSetAfterburners = false; + bool setAfterburnersEnabled = false; //[KSPField(guiActive = true, guiName = "Thrust")] public float debugThrust; public List multiModeEngines; + void Start() + { + axisGroupsModule = vessel.FindVesselModuleImplementingBDA(); // Look for an axis group module. + if (axisGroupsModule != null) hasAxisGroupsModule = true; + } + //[KSPEvent(guiActive = true, guiActiveEditor = false, guiName = "ToggleAC")] public void Toggle() { @@ -63,7 +77,7 @@ void AirspeedControl(FlightCtrlState s) { if (useBrakes) vessel.ActionGroups.SetGroup(KSPActionGroup.Brakes, true); - s.mainThrottle = 0; + SetThrottle(s, 0); return; } @@ -73,6 +87,11 @@ void AirspeedControl(FlightCtrlState s) float setAccel = speedError * throttleFactor; SetAcceleration(setAccel, s); + + if (forceAfterburnerIfMaxThrottle && s.mainThrottle == 1f) + SetAfterBurners(true); + else if (shouldSetAfterburners) + SetAfterBurners(setAfterburnersEnabled); } void SetAcceleration(float accel, FlightCtrlState s) @@ -87,12 +106,12 @@ void SetAcceleration(float accel, FlightCtrlState s) if (throttleOverride >= 0) { - s.mainThrottle = throttleOverride; + SetThrottle(s, throttleOverride); return; } if (engineAccel == 0) { - s.mainThrottle = accel > 0 ? 1 : 0; + SetThrottle(s, accel > 0 ? 1 : 0); return; } @@ -100,7 +119,7 @@ void SetAcceleration(float accel, FlightCtrlState s) float requestThrottle = (requestEngineAccel - dragAccel) / engineAccel; - s.mainThrottle = Mathf.Clamp01(requestThrottle); + SetThrottle(s, Mathf.Clamp01(requestThrottle)); //use brakes if overspeeding too much if (useBrakes) @@ -116,6 +135,20 @@ void SetAcceleration(float accel, FlightCtrlState s) } } + /// + /// Set the main throttle and the corresponding axis group. + /// + /// The flight control state + /// The throttle value + public void SetThrottle(FlightCtrlState s, float value) + { + s.mainThrottle = value; + if (hasAxisGroupsModule) + { + axisGroupsModule.UpdateAxisGroup(KSPAxisGroup.MainThrottle, 2f * value - 1f); // Throttle is full-axis: 0—1 throttle maps to -1—1 axis. + } + } + float MaxEngineAccel(float requestAccel, out float dragAccel) { float maxThrust = 0; @@ -167,18 +200,29 @@ float MaxEngineAccel(float requestAccel, out float dragAccel) allowAfterburner = allowAfterburner && (afterburnerPriority != 0f); //use multimode afterburner for extra accel if lacking - using (List.Enumerator mmes = multiModeEngines.GetEnumerator()) + if (allowAfterburner && (forceAfterburner || smoothedAccel < requestAccel * (1.5f / (Mathf.Exp(100f / 27f) - 1f) * (Mathf.Exp(Mathf.Clamp(afterburnerPriority, 0f, 100f) / 27f) - 1f)))) + { shouldSetAfterburners = true; setAfterburnersEnabled = true; } + else if (!allowAfterburner || (!forceAfterburner && smoothedAccel > requestAccel * (1f + 0.5f / (Mathf.Exp(50f / 25f) - 1f) * (Mathf.Exp(Mathf.Clamp(afterburnerPriority, 0f, 100f) / 25f) - 1f)))) + { shouldSetAfterburners = true; setAfterburnersEnabled = false; } + else + { shouldSetAfterburners = false; } + return accel; + } + + void SetAfterBurners(bool enable) + { + using (var mmes = multiModeEngines.GetEnumerator()) while (mmes.MoveNext()) { if (mmes.Current == null) continue; - if (allowAfterburner && (forceAfterburner || smoothedAccel < requestAccel * (1.5f / (Mathf.Exp(100f / 27f) - 1f) * (Mathf.Exp(Mathf.Clamp(afterburnerPriority, 0f, 100f) / 27f) - 1f)))) + if (enable) { if (mmes.Current.runningPrimary) { mmes.Current.Events["ModeEvent"].Invoke(); } } - else if (!allowAfterburner || (!forceAfterburner && smoothedAccel > requestAccel * (1f + 0.5f / (Mathf.Exp(50f / 25f) - 1f) * (Mathf.Exp(Mathf.Clamp(afterburnerPriority, 0f, 100f) / 25f) - 1f)))) + else { if (!mmes.Current.runningPrimary) { @@ -186,7 +230,6 @@ float MaxEngineAccel(float requestAccel, out float dragAccel) } } } - return accel; } private static bool IsAfterBurnerEngine(MultiModeEngine engine) @@ -196,6 +239,8 @@ private static bool IsAfterBurnerEngine(MultiModeEngine engine) return false; } return engine.primaryEngineID == "Dry" && engine.secondaryEngineID == "Wet"; + //presumably there's a reason this is looking specifically for MMEs with "Wet" and "Dry" as the IDs instead of !String.IsNullOrEmpty(engine.primaryEngineID). To permit only properly configured Jets? + } float GravAccel() @@ -219,12 +264,21 @@ public class BDLandSpeedControl : MonoBehaviour public Vessel vessel; public bool preventNegativeZeroPoint = false; + AxisGroupsModule axisGroupsModule; + bool hasAxisGroupsModule = false; // To avoid repeated null checks + private float lastThrottle; public float zeroPoint { get; private set; } private const float gain = 0.5f; private const float zeroMult = 0.02f; + void Start() + { + axisGroupsModule = vessel.FindVesselModuleImplementingBDA(); // Look for an axis group module. + if (axisGroupsModule != null) hasAxisGroupsModule = true; + } + public void Activate() { vessel.OnFlyByWire -= SpeedControl; @@ -241,11 +295,11 @@ public void Deactivate() void SpeedControl(FlightCtrlState s) { if (!vessel.LandedOrSplashed) - s.wheelThrottle = 0; + SetThrottle(s, 0); else if (targetSpeed == 0) { vessel.ActionGroups.SetGroup(KSPActionGroup.Brakes, true); - s.wheelThrottle = 0; + SetThrottle(s, 0); } else { @@ -253,10 +307,24 @@ void SpeedControl(FlightCtrlState s) lastThrottle = Mathf.Clamp(throttle, -1, 1); zeroPoint = (zeroPoint + lastThrottle * zeroMult) * (1 - zeroMult); if (preventNegativeZeroPoint && zeroPoint < 0) zeroPoint = 0; - s.wheelThrottle = lastThrottle; + SetThrottle(s, lastThrottle); vessel.ActionGroups.SetGroup(KSPActionGroup.Brakes, throttle < -5f); } } + + /// + /// Set the wheel throttle and the corresponding axis group. + /// + /// The flight control state + /// The throttle value + public void SetThrottle(FlightCtrlState s, float value) + { + s.wheelThrottle = value; + if (hasAxisGroupsModule) + { + axisGroupsModule.UpdateAxisGroup(KSPAxisGroup.MainThrottle, 2f * value - 1f); // Throttle is full-axis: 0—1 throttle maps to -1—1 axis. + } + } } public class BDVTOLSpeedControl : MonoBehaviour @@ -265,6 +333,9 @@ public class BDVTOLSpeedControl : MonoBehaviour public Vessel vessel; public bool preventNegativeZeroPoint = false; + AxisGroupsModule axisGroupsModule; + bool hasAxisGroupsModule = false; // To avoid repeated null checks + private float altIntegral; public float zeroPoint { get; private set; } @@ -272,6 +343,11 @@ public class BDVTOLSpeedControl : MonoBehaviour private const float Kd = 0.55f; private const float Ki = 0.03f; + void Start() + { + axisGroupsModule = vessel.FindVesselModuleImplementingBDA(); // Look for an axis group module. + if (axisGroupsModule != null) hasAxisGroupsModule = true; + } public void Activate() { @@ -291,7 +367,7 @@ void AltitudeControl(FlightCtrlState s) if (targetAltitude == 0) { vessel.ActionGroups.SetGroup(KSPActionGroup.Brakes, true); - s.mainThrottle = 0; + SetThrottle(s, 0); } else { @@ -299,12 +375,26 @@ void AltitudeControl(FlightCtrlState s) float altP = Kp * (targetAltitude - (float)vessel.radarAltitude); float altD = Kd * (float)vessel.verticalSpeed; altIntegral = Ki * Mathf.Clamp(altIntegral + altError * Time.deltaTime, -1f, 1f); - + float throttle = altP + altIntegral - altD; - s.mainThrottle = Mathf.Clamp01(throttle); + SetThrottle(s, Mathf.Clamp01(throttle)); vessel.ActionGroups.SetGroup(KSPActionGroup.Brakes, throttle < -5f); } } + + /// + /// Set the main throttle and the corresponding axis group. + /// + /// The flight control state + /// The throttle value + public void SetThrottle(FlightCtrlState s, float value) + { + s.mainThrottle = value; + if (hasAxisGroupsModule) + { + axisGroupsModule.UpdateAxisGroup(KSPAxisGroup.MainThrottle, 2f * value - 1f); // Throttle is full-axis: 0—1 throttle maps to -1—1 axis. + } + } } } diff --git a/BDArmory/Modules/BDGenericAIBase.cs b/BDArmory/Control/BDGenericAIBase.cs similarity index 54% rename from BDArmory/Modules/BDGenericAIBase.cs rename to BDArmory/Control/BDGenericAIBase.cs index 01d80f412..c00d8d501 100644 --- a/BDArmory/Modules/BDGenericAIBase.cs +++ b/BDArmory/Control/BDGenericAIBase.cs @@ -1,14 +1,18 @@ -using System; +using UnityEngine; +using System.Collections; +using System.Collections.Generic; +using System.Linq; using System.Text; -using BDArmory.Control; -using BDArmory.Core; -using BDArmory.Misc; + +using BDArmory.Competition; +using BDArmory.Extensions; +using BDArmory.GameModes.Waypoints; +using BDArmory.Settings; using BDArmory.Targeting; using BDArmory.UI; -using UnityEngine; -using KSP.Localization; +using BDArmory.Utils; -namespace BDArmory.Modules +namespace BDArmory.Control { /// /// A base class for implementing AI. @@ -32,6 +36,9 @@ public abstract class BDGenericAIBase : PartModule, IBDAIControl, IBDWMModule /// protected BDAirspeedControl speedController; + protected bool hasAxisGroupsModule = false; + protected AxisGroupsModule axisGroupsModule; + protected Transform vesselTransform => vessel.ReferenceTransform; protected StringBuilder debugString = new StringBuilder(); @@ -56,6 +63,7 @@ protected set public ModuleWingCommander commandLeader { get; protected set; } protected PilotCommands command; + PilotCommands previousCommand; public string currentStatus { get; protected set; } = "Free"; protected int commandFollowIndex; @@ -106,6 +114,26 @@ private void autoPilot(FlightCtrlState s) AutoPilot(s); } + /// + /// Set the flight control state and also the corresponding axis groups. + /// + /// The flight control state + /// pitch + /// yaw + /// roll + protected virtual void SetFlightControlState(FlightCtrlState s, float pitch, float yaw, float roll) + { + s.pitch = pitch; + s.yaw = yaw; + s.roll = roll; + if (hasAxisGroupsModule) + { + axisGroupsModule.UpdateAxisGroup(KSPAxisGroup.Pitch, pitch); + axisGroupsModule.UpdateAxisGroup(KSPAxisGroup.Yaw, yaw); + axisGroupsModule.UpdateAxisGroup(KSPAxisGroup.Roll, roll); + } + } + #region Pilot on/off public virtual void ActivatePilot() @@ -128,8 +156,15 @@ public virtual void ActivatePilot() GameEvents.onVesselDestroy.Add(RemoveAutopilot); assignedPositionWorld = vessel.ReferenceTransform.position; - // I need to make sure gear is deployed on startup so it'll get properly retracted. - vessel.ActionGroups.SetGroup(KSPActionGroup.Gear, true); + try // Sometimes the FSM breaks trying to set the gear action group + { + // I need to make sure gear is deployed on startup so it'll get properly retracted. + vessel.ActionGroups.SetGroup(KSPActionGroup.Gear, true); + } + catch (System.Exception e) + { + Debug.LogError($"[BDArmory.BDGenericAIBase]: Failed to set Gear action group: {e.Message}"); + } RefreshPartWindow(); } @@ -156,7 +191,7 @@ protected void RemoveAutopilot(Vessel v) protected void RefreshPartWindow() { - Events["TogglePilot"].guiName = pilotEnabled ? Localizer.Format("#LOC_BDArmory_DeactivatePilot") : Localizer.Format("#LOC_BDArmory_ActivatePilot");//"Deactivate Pilot""Activate Pilot" + Events["TogglePilot"].guiName = pilotEnabled ? StringUtils.Localize("#LOC_BDArmory_DeactivatePilot") : StringUtils.Localize("#LOC_BDArmory_ActivatePilot");//"Deactivate Pilot""Activate Pilot" } [KSPEvent(guiActive = true, guiName = "#LOC_BDArmory_TogglePilot", active = true)]//Toggle Pilot @@ -202,6 +237,8 @@ protected virtual void Start() activeVessel = vessel; UpdateWeaponManager(); + axisGroupsModule = vessel.FindVesselModuleImplementingBDA(); // Look for an axis group module so we can set the axis groups when setting the flight control state. + if (axisGroupsModule != null) hasAxisGroupsModule = true; if (pilotEnabled) { @@ -226,6 +263,7 @@ protected virtual void OnDestroy() part.OnJustAboutToBeDestroyed -= DeactivatePilot; if (vessel != null) vessel.OnJustAboutToBeDestroyed -= DeactivatePilot; GameEvents.onVesselWasModified.Remove(onVesselWasModified); + GameEvents.onVesselDestroy.Remove(RemoveAutopilot); MissileFire.OnChangeTeam -= OnToggleTeam; GameEvents.onPartDie.Remove(OnPartDie); } @@ -233,7 +271,7 @@ protected virtual void OnDestroy() protected virtual void OnGUI() { if (!pilotEnabled || !vessel.isActiveVessel) return; - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) { GUI.Label(new Rect(200, Screen.height - 300, 600, 300), $"{vessel.name}\n{debugString.ToString()}"); } @@ -355,25 +393,31 @@ protected void SetStatus(string text) #region WingCommander - public virtual void ReleaseCommand() + public virtual void ReleaseCommand(bool resetAssignedPosition = true, bool storeCommand = true) { if (!vessel || command == PilotCommands.Free) return; - if (command == PilotCommands.Follow && commandLeader) - { - commandLeader = null; - } - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BDGenericAIBase]:" + vessel.vesselName + " was released from command."); + if (BDArmorySettings.DEBUG_AI) Debug.Log("[BDArmory.BDGenericAIBase]:" + vessel.vesselName + " was released from command."); + previousCommand = command; command = PilotCommands.Free; - assignedPositionWorld = vesselTransform.position; + if (!storeCommand) // Clear the previous command. + { + if (previousCommand == PilotCommands.Follow) commandLeader = null; + previousCommand = PilotCommands.Free; + } + if (resetAssignedPosition) // Clear the assigned position. + { + assignedPositionWorld = vesselTransform.position; + } } public virtual void CommandFollow(ModuleWingCommander leader, int followerIndex) { if (!pilotEnabled) return; - if (leader == vessel || followerIndex < 0) return; + if (leader is null || leader == vessel || followerIndex < 0) return; - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BDGenericAIBase]:" + vessel.vesselName + " was commanded to follow."); + if (BDArmorySettings.DEBUG_AI) Debug.Log("[BDArmory.BDGenericAIBase]:" + vessel.vesselName + " was commanded to follow."); + previousCommand = command; command = PilotCommands.Follow; commandLeader = leader; commandFollowIndex = followerIndex; @@ -389,8 +433,9 @@ public virtual void CommandFlyTo(Vector3 gpsCoords) { if (!pilotEnabled) return; - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BDGenericAIBase]:" + vessel.vesselName + " was commanded to go to."); + if (BDArmorySettings.DEBUG_AI) Debug.Log($"[BDArmory.BDGenericAIBase]: {vessel.vesselName} was commanded to go to {gpsCoords}."); assignedPositionGeo = gpsCoords; + previousCommand = command; command = PilotCommands.FlyTo; } @@ -398,8 +443,9 @@ public virtual void CommandAttack(Vector3 gpsCoords) { if (!pilotEnabled) return; - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BDGenericAIBase]:" + vessel.vesselName + " was commanded to attack."); + if (BDArmorySettings.DEBUG_AI) Debug.Log($"[BDArmory.BDGenericAIBase]: {vessel.vesselName} was commanded to attack {gpsCoords}."); assignedPositionGeo = gpsCoords; + previousCommand = command; command = PilotCommands.Attack; } @@ -412,10 +458,157 @@ public virtual void CommandFollowWaypoints() { if (!pilotEnabled) return; // Do nothing if we haven't taken off (or activated with airspawn) yet. - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BDGenericAIBase]:" + vessel.vesselName + " was commanded to follow waypoints."); + if (BDArmorySettings.DEBUG_AI) Debug.Log("[BDArmory.BDGenericAIBase]:" + vessel.vesselName + " was commanded to follow waypoints."); + previousCommand = command; command = PilotCommands.Waypoints; } + /// + /// Resume a previous command. + /// ReleaseCommand should be called with resetAssignedPosition=false if the previous command is to be preserved. + /// + /// true if the previous command is resumed, false otherwise. + public virtual bool ResumeCommand() + { + switch (previousCommand) + { + case PilotCommands.Free: + return false; + case PilotCommands.Attack: + CommandAttack(assignedPositionGeo); + break; + case PilotCommands.FlyTo: + CommandFlyTo(assignedPositionGeo); + break; + case PilotCommands.Follow: + CommandFollow(commandLeader, commandFollowIndex); + break; + case PilotCommands.Waypoints: + CommandFollowWaypoints(); + break; + } + return true; + } #endregion WingCommander + + #region Waypoints + protected List waypoints = null; + protected int waypointCourseIndex = 0; + protected int activeWaypointIndex = -1; + protected int activeWaypointLap = 1; + protected int waypointLapLimit = 1; + protected Vector3 waypointPosition = default; + //protected float waypointRadius = 500f; + public float waypointRange = 999f; + + public bool IsRunningWaypoints => command == PilotCommands.Waypoints && + activeWaypointLap <= waypointLapLimit && + activeWaypointIndex >= 0 && + waypoints != null && + waypoints.Count > 0; + public int CurrentWaypointIndex => this.activeWaypointIndex; + + public void ClearWaypoints() + { + if (BDArmorySettings.DEBUG_AI) Debug.Log("[BDArmory.BDGenericAIBase]: Cleared waypoints"); + this.waypoints = null; + this.activeWaypointIndex = -1; + } + + public void SetWaypoints(List waypoints) + { + if (waypoints == null || waypoints.Count == 0) + { + this.activeWaypointIndex = -1; + this.waypoints = null; + return; + } + if (BDArmorySettings.DEBUG_AI) Debug.Log(string.Format("[BDArmory.BDGenericAIBase]: Set {0} waypoints", waypoints.Count)); + this.waypoints = waypoints; + this.waypointCourseIndex = BDArmorySettings.WAYPOINT_COURSE_INDEX; + this.activeWaypointIndex = 0; + this.activeWaypointLap = 1; + this.waypointLapLimit = BDArmorySettings.WAYPOINT_LOOP_INDEX; + var waypoint = waypoints[activeWaypointIndex]; + var terrainAltitude = FlightGlobals.currentMainBody.TerrainAltitude(waypoint.x, waypoint.y); + waypointPosition = FlightGlobals.currentMainBody.GetWorldSurfacePosition(waypoint.x, waypoint.y, waypoint.z + terrainAltitude); + CommandFollowWaypoints(); + } + + protected virtual void UpdateWaypoint() + { + if (activeWaypointIndex < 0 || waypoints == null || waypoints.Count == 0) + { + if (command == PilotCommands.Waypoints) ReleaseCommand(); + return; + } + var waypoint = waypoints[activeWaypointIndex]; + var terrainAltitude = FlightGlobals.currentMainBody.TerrainAltitude(waypoint.x, waypoint.y); + waypointPosition = FlightGlobals.currentMainBody.GetWorldSurfacePosition(waypoint.x, waypoint.y, waypoint.z + terrainAltitude); + waypointRange = (float)(vesselTransform.position - waypointPosition).magnitude; + var timeToCPA = AIUtils.TimeToCPA(vessel.transform.position - waypointPosition, vessel.Velocity(), vessel.acceleration, Time.fixedDeltaTime); + if (waypointRange < WaypointCourses.CourseLocations[waypointCourseIndex].waypoints[activeWaypointIndex].scale && timeToCPA < Time.fixedDeltaTime) // Within waypointRadius and reaching a minimum within the next frame. Looking forwards like this avoids a frame where the fly-to direction is backwards allowing smoother waypoint traversal. + { + // moving away, proceed to next point + var deviation = AIUtils.PredictPosition(vessel.transform.position - waypointPosition, vessel.Velocity(), vessel.acceleration, timeToCPA).magnitude; + if (BDArmorySettings.DEBUG_AI) Debug.Log(string.Format("[BDArmory.BDGenericAIBase]: Reached waypoint {0} with range {1}", activeWaypointIndex, deviation)); + BDACompetitionMode.Instance.Scores.RegisterWaypointReached(vessel.vesselName, waypointCourseIndex, activeWaypointIndex, activeWaypointLap, waypointLapLimit, deviation); + + if (BDArmorySettings.WAYPOINT_GUARD_INDEX >= 0 && activeWaypointIndex >= BDArmorySettings.WAYPOINT_GUARD_INDEX && !weaponManager.guardMode) + { + // activate guard mode + weaponManager.guardMode = true; + } + + ++activeWaypointIndex; + if (activeWaypointIndex >= waypoints.Count && activeWaypointLap > waypointLapLimit) + { + if (BDArmorySettings.DEBUG_AI) Debug.Log("[BDArmory.BDGenericAIBase]: Waypoints complete"); + waypoints = null; + ReleaseCommand(); + return; + } + else if (activeWaypointIndex >= waypoints.Count && activeWaypointLap <= waypointLapLimit) + { + activeWaypointIndex = 0; + activeWaypointLap++; + } + UpdateWaypoint(); // Call ourselves again for the new waypoint to follow. + } + } + + Coroutine maintainingFuelLevelsCoroutine; + /// + /// Prevent fuel resource drain until the next waypoint. + /// + public void MaintainFuelLevelsUntilWaypoint() + { + if (maintainingFuelLevelsCoroutine != null) StopCoroutine(maintainingFuelLevelsCoroutine); + maintainingFuelLevelsCoroutine = StartCoroutine(MaintainFuelLevelsUntilWaypointCoroutine()); + } + /// + /// Prevent fuel resource drain until the next waypoint (coroutine). + /// Note: this should probably use the non-waypoint version below and just start/stop it based on the waypoint index. + /// + IEnumerator MaintainFuelLevelsUntilWaypointCoroutine() + { + if (vessel == null) yield break; + var vesselName = vessel.vesselName; + var wait = new WaitForFixedUpdate(); + var fuelResourceParts = new Dictionary>(); + var currentWaypointIndex = CurrentWaypointIndex; + ResourceUtils.DeepFind(vessel.rootPart, ResourceUtils.FuelResources, fuelResourceParts, true); + var fuelResources = fuelResourceParts.ToDictionary(t => t.Key, t => t.Value.ToDictionary(p => p, p => p.amount)); + while (vessel != null && IsRunningWaypoints && CurrentWaypointIndex == currentWaypointIndex) + { + foreach (var fuelResource in fuelResources.Values) + { + foreach (var partResource in fuelResource.Keys) + { partResource.amount = fuelResource[partResource]; } + } + yield return wait; + } + } + #endregion } } diff --git a/BDArmory/Modules/BDModulePilotAI.cs b/BDArmory/Control/BDModulePilotAI.cs similarity index 51% rename from BDArmory/Modules/BDModulePilotAI.cs rename to BDArmory/Control/BDModulePilotAI.cs index a1052f90c..ee42433c7 100644 --- a/BDArmory/Modules/BDModulePilotAI.cs +++ b/BDArmory/Control/BDModulePilotAI.cs @@ -4,18 +4,18 @@ using System.Linq; using System.Reflection; using System.Text; +using UnityEngine; + using BDArmory.Competition; -using BDArmory.Control; -using BDArmory.Core; -using BDArmory.Core.Extension; -using BDArmory.Core.Utils; +using BDArmory.Extensions; using BDArmory.Guidances; -using BDArmory.Misc; -using BDArmory.Targeting; +using BDArmory.Settings; using BDArmory.UI; -using UnityEngine; +using BDArmory.Utils; +using BDArmory.Weapons; +using BDArmory.Weapons.Missiles; -namespace BDArmory.Modules +namespace BDArmory.Control { public class BDModulePilotAI : BDGenericAIBase, IBDAIControl { @@ -24,9 +24,14 @@ public enum SteerModes SteerModes steerMode = SteerModes.NormalFlight; + public float FlatSpin = 0; // 0 is not in FlatSpin, -1 is clockwise spin, 1 is counter-clockwise spin (set up this way instead of bool to allow future implementation for asymmetric thrust) + float flatSpinStartTime = float.MaxValue; + bool isPSM = false; // Is the plane doing post-stall manoeuvering? Note: this isn't really being used for anything other than debugging at the moment. + bool extending; bool extendParametersSet = false; float extendDistance; + float lastExtendDistanceSqr = 0; bool extendHorizontally = true; // Measure the extendDistance horizonally (for A2G) or not (for A2A). float desiredMinAltitude; public string extendingReason = ""; @@ -34,6 +39,9 @@ public enum SteerModes bool requestedExtend; Vector3 requestedExtendTpos; + float extendRequestMinDistance = 0; + MissileBase extendForMissile = null; + float extendAbortTimer = 0; public bool IsExtending { @@ -44,12 +52,18 @@ public bool IsExtending bool wasEvading = false; public bool IsEvading => evading; - public void StopExtending(string reason) + public void StopExtending(string reason, bool cooldown = false) { + if (!extending) return; extending = false; + requestedExtend = false; extendingReason = ""; extendTarget = null; - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log($"[BDArmory.BDModulePilotAI]: {vessel.vesselName} stopped extending due to {reason}."); + extendRequestMinDistance = 0; + extendAbortTimer = cooldown ? -5f : 0f; + lastExtendDistanceSqr = 0; + extendForMissile = null; + if (BDArmorySettings.DEBUG_AI) Debug.Log($"[BDArmory.BDModulePilotAI]: {Time.time:F3} {vessel.vesselName} stopped extending due to {reason}."); } /// @@ -58,14 +72,29 @@ public void StopExtending(string reason) /// /// Reason for extending /// The target to extend from + /// The minimum distance to extend for /// The position to extend from if the target is null - public void RequestExtend(string reason = "requested", Vessel target = null, Vector3 tPosition = default) + /// The missile to fire if extending to fire a missile + /// Override the cooldown period + public void RequestExtend(string reason = "requested", Vessel target = null, float minDistance = 0, Vector3 tPosition = default, MissileBase missile = null, bool ignoreCooldown = false) { + if (ignoreCooldown) extendAbortTimer = 0f; // Disable the cooldown. + else if (extendAbortTimer < 0) return; // Ignore request while in cooldown. requestedExtend = true; extendTarget = target; + extendRequestMinDistance = minDistance; requestedExtendTpos = extendTarget != null ? target.CoM : tPosition; + extendForMissile = missile; extendingReason = reason; } + public void DebugExtending() // Debug being stuck in extending (enable DEBUG_AI, then click the "Debug Extending" button) + { + if (!extending) return; + var extendVector = extendHorizontally ? (vessel.transform.position - lastTargetPosition).ProjectOnPlanePreNormalized(upDirection) : vessel.transform.position - lastTargetPosition; + var message = $"{vessel.vesselName} is extending due to: {extendingReason}, extendTarget: {extendTarget}, distance: {extendVector.magnitude}m of {extendDistance}m {(extendHorizontally ? "horizontally" : "total")}"; + BDACompetitionMode.Instance.competitionStatus.Add(message); + Debug.Log($"DEBUG EXTENDING {message}"); + } public override bool CanEngage() { @@ -92,13 +121,11 @@ Transform velocityTransform Vector3 upDirection = Vector3.up; #region Pilot AI Settings GUI - #region PID [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_SteerFactor", //Steer Factor groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_PilotAI_PID", groupStartCollapsed = true), UI_FloatRange(minValue = 0.1f, maxValue = 20f, stepIncrement = 0.1f, scene = UI_Scene.All)] public float steerMult = 14f; - //make a combat steer mult and idle steer mult [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_SteerKi", //Steer Ki groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_PilotAI_PID", groupStartCollapsed = true), @@ -107,10 +134,22 @@ Transform velocityTransform [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_SteerDamping", //Steer Damping groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_PilotAI_PID", groupStartCollapsed = true), - UI_FloatRange(minValue = 1f, maxValue = 8f, stepIncrement = 0.1f, scene = UI_Scene.All)] + UI_FloatRange(minValue = 0.1f, maxValue = 8f, stepIncrement = 0.1f, scene = UI_Scene.All)] public float steerDamping = 5f; #region Dynamic Damping + //Toggle Dynamic Steer Damping + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_DynamicSteerDamping", advancedTweakable = true, + groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_PilotAI_PID", groupStartCollapsed = true), + UI_Toggle(scene = UI_Scene.All, disabledText = "#LOC_BDArmory_Disabled", enabledText = "#LOC_BDArmory_Enabled")] + public bool dynamicSteerDamping = false; + + //Toggle 3-Axis Dynamic Steer Damping + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_3AxisDynamicSteerDamping", advancedTweakable = true, + groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_PilotAI_PID", groupStartCollapsed = true), + UI_Toggle(enabledText = "#LOC_BDArmory_Enabled", disabledText = "#LOC_BDArmory_Disabled", scene = UI_Scene.All)] + public bool CustomDynamicAxisFields = true; + // Note: min/max is replaced by off-target/on-target in localisation, but the variable names are kept to avoid reconfiguring existing craft. // Dynamic Damping [KSPField(guiName = "#LOC_BDArmory_DynamicDamping", groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_PilotAI_PID", groupStartCollapsed = true), UI_Label(scene = UI_Scene.All)] @@ -118,12 +157,12 @@ Transform velocityTransform [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_DynamicDampingMin", advancedTweakable = true, groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_PilotAI_PID", groupStartCollapsed = true), - UI_FloatRange(minValue = 1f, maxValue = 8f, stepIncrement = 0.1f, scene = UI_Scene.All)] + UI_FloatRange(minValue = 0.1f, maxValue = 8f, stepIncrement = 0.1f, scene = UI_Scene.All)] public float DynamicDampingMin = 6f; [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_DynamicDampingMax", advancedTweakable = true, groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_PilotAI_PID", groupStartCollapsed = true), - UI_FloatRange(minValue = 1f, maxValue = 8f, stepIncrement = 0.1f, scene = UI_Scene.All)] + UI_FloatRange(minValue = 0.1f, maxValue = 8f, stepIncrement = 0.1f, scene = UI_Scene.All)] public float DynamicDampingMax = 6.7f; [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_DynamicDampingFactor", advancedTweakable = true, @@ -142,12 +181,12 @@ Transform velocityTransform [KSPField(isPersistant = true, guiName = "#LOC_BDArmory_DynamicDampingPitchMin", advancedTweakable = true, //Dynamic steer damping Clamp min groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_PilotAI_PID", groupStartCollapsed = true), - UI_FloatRange(minValue = 1f, maxValue = 8f, stepIncrement = 0.1f, scene = UI_Scene.All)] + UI_FloatRange(minValue = 0.1f, maxValue = 8f, stepIncrement = 0.1f, scene = UI_Scene.All)] public float DynamicDampingPitchMin = 6f; [KSPField(isPersistant = true, guiName = "#LOC_BDArmory_DynamicDampingPitchMax", advancedTweakable = true, //Dynamic steer damping Clamp max groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_PilotAI_PID", groupStartCollapsed = true), - UI_FloatRange(minValue = 1f, maxValue = 8f, stepIncrement = 0.1f, scene = UI_Scene.All)] + UI_FloatRange(minValue = 0.1f, maxValue = 8f, stepIncrement = 0.1f, scene = UI_Scene.All)] public float DynamicDampingPitchMax = 6.5f; [KSPField(isPersistant = true, guiName = "#LOC_BDArmory_DynamicDampingPitchFactor", advancedTweakable = true, @@ -166,12 +205,12 @@ Transform velocityTransform [KSPField(isPersistant = true, guiName = "#LOC_BDArmory_DynamicDampingYawMin", advancedTweakable = true, //Dynamic steer damping Clamp min groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_PilotAI_PID", groupStartCollapsed = true), - UI_FloatRange(minValue = 1f, maxValue = 8f, stepIncrement = 0.1f, scene = UI_Scene.All)] + UI_FloatRange(minValue = 0.1f, maxValue = 8f, stepIncrement = 0.1f, scene = UI_Scene.All)] public float DynamicDampingYawMin = 6f; [KSPField(isPersistant = true, guiName = "#LOC_BDArmory_DynamicDampingYawMax", advancedTweakable = true, //Dynamic steer damping Clamp max groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_PilotAI_PID", groupStartCollapsed = true), - UI_FloatRange(minValue = 1f, maxValue = 8f, stepIncrement = 0.1f, scene = UI_Scene.All)] + UI_FloatRange(minValue = 0.1f, maxValue = 8f, stepIncrement = 0.1f, scene = UI_Scene.All)] public float DynamicDampingYawMax = 6.5f; [KSPField(isPersistant = true, guiName = "#LOC_BDArmory_DynamicDampingYawFactor", advancedTweakable = true, @@ -190,30 +229,88 @@ Transform velocityTransform [KSPField(isPersistant = true, guiName = "#LOC_BDArmory_DynamicDampingRollMin", advancedTweakable = true, groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_PilotAI_PID", groupStartCollapsed = true), - UI_FloatRange(minValue = 1f, maxValue = 8f, stepIncrement = 0.1f, scene = UI_Scene.All)] + UI_FloatRange(minValue = 0.1f, maxValue = 8f, stepIncrement = 0.1f, scene = UI_Scene.All)] public float DynamicDampingRollMin = 6f; [KSPField(isPersistant = true, guiName = "#LOC_BDArmory_DynamicDampingRollMax", advancedTweakable = true, groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_PilotAI_PID", groupStartCollapsed = true), - UI_FloatRange(minValue = 1f, maxValue = 8f, stepIncrement = 0.1f, scene = UI_Scene.All)] + UI_FloatRange(minValue = 0.1f, maxValue = 8f, stepIncrement = 0.1f, scene = UI_Scene.All)] public float DynamicDampingRollMax = 6.5f; [KSPField(isPersistant = true, guiName = "#LOC_BDArmory_DynamicDampingRollFactor", advancedTweakable = true, //Dynamic steer dampening Factor groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_PilotAI_PID", groupStartCollapsed = true), UI_FloatRange(minValue = 0.1f, maxValue = 10f, stepIncrement = 0.1f, scene = UI_Scene.All)] public float dynamicSteerDampingRollFactor = 8f; + #endregion - //Toggle Dynamic Steer Damping - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_DynamicSteerDamping", advancedTweakable = true, + #region AutoTuning + //Toggle AutoTuning + [KSPField(isPersistant = false, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_PIDAutoTune", advancedTweakable = true, groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_PilotAI_PID", groupStartCollapsed = true), - UI_Toggle(scene = UI_Scene.All, disabledText = "#LOC_BDArmory_Disabled", enabledText = "#LOC_BDArmory_Enabled")] - public bool dynamicSteerDamping = false; + UI_Toggle(enabledText = "#LOC_BDArmory_Enabled", disabledText = "#LOC_BDArmory_Disabled", scene = UI_Scene.All)] + bool autoTune = false; + public bool AutoTune { get { return autoTune; } set { autoTune = value; OnAutoTuneChanged(null, null); } } + public PIDAutoTuning pidAutoTuning; + + [KSPField(isPersistant = false, guiActive = false, guiActiveEditor = false, guiName = "#LOC_BDArmory_AutoTuningLoss", groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_PilotAI_PID", groupStartCollapsed = true), UI_Label(scene = UI_Scene.All)] + public string autoTuningLossLabel = ""; + [KSPField(isPersistant = false, guiActive = false, guiActiveEditor = false, guiName = "\tParams", groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_PilotAI_PID", groupStartCollapsed = true), UI_Label(scene = UI_Scene.All)] + public string autoTuningLossLabel2 = ""; + [KSPField(isPersistant = false, guiActive = false, guiActiveEditor = false, guiName = "\tField", groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_PilotAI_PID", groupStartCollapsed = true), UI_Label(scene = UI_Scene.All)] + public string autoTuningLossLabel3 = ""; + + //AutoTuning Number Of Samples + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = false, guiName = "#LOC_BDArmory_PIDAutoTuningNumSamples", advancedTweakable = true, + groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_PilotAI_PID", groupStartCollapsed = true), + UI_FloatRange(minValue = 1f, maxValue = 10f, stepIncrement = 1f, scene = UI_Scene.All)] + public float autoTuningOptionNumSamples = 5f; - //Toggle 3-Axis Dynamic Steer Damping - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_3AxisDynamicSteerDamping", advancedTweakable = true, + //AutoTuning Fast Response Relevance + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = false, guiName = "#LOC_BDArmory_PIDAutoTuningFastResponseRelevance", advancedTweakable = true, + groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_PilotAI_PID", groupStartCollapsed = true), + UI_FloatRange(minValue = 0f, maxValue = 0.5f, stepIncrement = 0.01f, scene = UI_Scene.All)] + public float autoTuningOptionFastResponseRelevance = 0.2f; + + //AutoTuning Initial Learning Rate + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = false, guiName = "#LOC_BDArmory_PIDAutoTuningInitialLearningRate", advancedTweakable = true, + groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_PilotAI_PID", groupStartCollapsed = true), + UI_FloatLogRange(minValue = 0.001f, maxValue = 1f, steps = 6, scene = UI_Scene.All)] + public float autoTuningOptionInitialLearningRate = 1f; + + //AutoTuning Altitude + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = false, guiName = "#LOC_BDArmory_PIDAutoTuningAltitude", //Auto-tuning Altitude + groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_PilotAI_PID", groupStartCollapsed = true), + UI_FloatRange(minValue = 50f, maxValue = 5000f, stepIncrement = 50f, scene = UI_Scene.All)] + public float autoTuningAltitude = 1000f; + + //AutoTuning Speed + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = false, guiName = "#LOC_BDArmory_PIDAutoTuningSpeed", //Auto-tuning Speed + groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_PilotAI_PID", groupStartCollapsed = true), + UI_FloatRange(minValue = 50f, maxValue = 800f, stepIncrement = 5.0f, scene = UI_Scene.All)] + public float autoTuningSpeed = 200f; + + // Fixed fields for auto-tuning (only accessible via the AI GUI for now) + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = false)] public bool autoTuningOptionFixedP = false; + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = false)] public bool autoTuningOptionFixedI = false; + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = false)] public bool autoTuningOptionFixedD = false; + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = false)] public bool autoTuningOptionFixedDOff = false; + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = false)] public bool autoTuningOptionFixedDOn = false; + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = false)] public bool autoTuningOptionFixedDF = false; + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = false)] public bool autoTuningOptionFixedDPOff = false; + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = false)] public bool autoTuningOptionFixedDPOn = false; + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = false)] public bool autoTuningOptionFixedDPF = false; + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = false)] public bool autoTuningOptionFixedDYOff = false; + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = false)] public bool autoTuningOptionFixedDYOn = false; + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = false)] public bool autoTuningOptionFixedDYF = false; + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = false)] public bool autoTuningOptionFixedDROff = false; + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = false)] public bool autoTuningOptionFixedDROn = false; + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = false)] public bool autoTuningOptionFixedDRF = false; + + //Clamp Maximums + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = false, guiName = "#LOC_BDArmory_PIDAutoTuningClampMaximums", advancedTweakable = true, groupName = "pilotAI_PID", groupDisplayName = "#LOC_BDArmory_PilotAI_PID", groupStartCollapsed = true), UI_Toggle(enabledText = "#LOC_BDArmory_Enabled", disabledText = "#LOC_BDArmory_Disabled", scene = UI_Scene.All)] - public bool CustomDynamicAxisFields = true; + public bool autoTuningOptionClampMaximums = false; #endregion #endregion @@ -269,6 +366,11 @@ Transform velocityTransform groupName = "pilotAI_Speeds", groupDisplayName = "#LOC_BDArmory_PilotAI_Speeds", groupStartCollapsed = true), UI_FloatRange(minValue = 0f, maxValue = 100f, stepIncrement = 1.0f, scene = UI_Scene.All)] public float ABPriority = 50f; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_ABOverrideThreshold", advancedTweakable = true, //Afterburner Override Threshold + groupName = "pilotAI_Speeds", groupDisplayName = "#LOC_BDArmory_PilotAI_Speeds", groupStartCollapsed = true), + UI_FloatRange(minValue = 0f, maxValue = 200f, stepIncrement = 1.0f, scene = UI_Scene.All)] + public float ABOverrideThreshold = 0f; #endregion #region Control Limits @@ -292,6 +394,16 @@ Transform velocityTransform UI_FloatRange(minValue = 10f, maxValue = 500f, stepIncrement = 1.0f, scene = UI_Scene.All)] public float cornerSpeed = 200f; + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AltitudeSteerLimiterFactor", advancedTweakable = true, // Altitude Steer Limiter Factor + groupName = "pilotAI_ControlLimits", groupDisplayName = "#LOC_BDArmory_PilotAI_ControlLimits", groupStartCollapsed = true), + UI_FloatRange(minValue = -1f, maxValue = 1f, stepIncrement = .05f, scene = UI_Scene.All)] + public float altitudeSteerLimiterFactor = 0f; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AltitudeSteerLimiterAltitude", advancedTweakable = true, // Altitude Steer Limiter Altitude + groupName = "pilotAI_ControlLimits", groupDisplayName = "#LOC_BDArmory_PilotAI_ControlLimits", groupStartCollapsed = true), + UI_FloatRange(minValue = 100f, maxValue = 10000f, stepIncrement = 100f, scene = UI_Scene.All)] + public float altitudeSteerLimiterAltitude = 5000f; + //[KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_AttitudeLimiter", advancedTweakable = true, //Attitude Limiter, not currently functional // groupName = "pilotAI_ControlLimits", groupDisplayName = "#LOC_BDArmory_PilotAI_ControlLimits", groupStartCollapsed = true), // UI_FloatRange(minValue = 10f, maxValue = 90f, stepIncrement = 5f, scene = UI_Scene.All)] @@ -319,8 +431,19 @@ Transform velocityTransform [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_maxAllowedAoA", //Max AoA groupName = "pilotAI_ControlLimits", groupDisplayName = "#LOC_BDArmory_PilotAI_ControlLimits", groupStartCollapsed = true), - UI_FloatRange(minValue = 0f, maxValue = 85f, stepIncrement = 2.5f, scene = UI_Scene.All)] + UI_FloatRange(minValue = 0f, maxValue = 90f, stepIncrement = 2.5f, scene = UI_Scene.All)] public float maxAllowedAoA = 35; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_postStallAoA", //Post-stall AoA + groupName = "pilotAI_ControlLimits", groupDisplayName = "#LOC_BDArmory_PilotAI_ControlLimits", groupStartCollapsed = true), + UI_FloatRange(minValue = 0f, maxValue = 90f, stepIncrement = 2.5f, scene = UI_Scene.All)] + public float postStallAoA = 35; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_ImmelmannTurnAngle", advancedTweakable = true, // Immelmann Turn Angle + groupName = "pilotAI_ControlLimits", groupDisplayName = "#LOC_BDArmory_PilotAI_ControlLimits", groupStartCollapsed = true), + UI_FloatRange(minValue = 0f, maxValue = 90f, stepIncrement = 1f, scene = UI_Scene.All)] + public float ImmelmannTurnAngle = 30f; // 30° from directly behind -> 150° + float ImmelmannTurnCosAngle = -0.866f; #endregion #region EvadeExtend @@ -329,12 +452,6 @@ Transform velocityTransform UI_FloatRange(minValue = 0f, maxValue = 1f, stepIncrement = .05f, scene = UI_Scene.All)] public float minEvasionTime = 0.2f; - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_EvasionNonlinearity", advancedTweakable = true, // Evasion/Extension Nonlinearity - groupName = "pilotAI_EvadeExtend", groupDisplayName = "#LOC_BDArmory_PilotAI_EvadeExtend", groupStartCollapsed = true), - UI_FloatRange(minValue = 0f, maxValue = 10f, stepIncrement = .1f, scene = UI_Scene.All)] - public float evasionNonlinearity = 2f; - float evasionNonlinearityDirection = 1; - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_EvasionThreshold", advancedTweakable = true, //Evade Threshold groupName = "pilotAI_EvadeExtend", groupDisplayName = "#LOC_BDArmory_PilotAI_EvadeExtend", groupStartCollapsed = true), UI_FloatRange(minValue = 0f, maxValue = 100f, stepIncrement = 1f, scene = UI_Scene.All)] @@ -342,9 +459,15 @@ Transform velocityTransform [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_EvasionTimeThreshold", advancedTweakable = true, // Time on Target Threshold groupName = "pilotAI_EvadeExtend", groupDisplayName = "#LOC_BDArmory_PilotAI_EvadeExtend", groupStartCollapsed = true), - UI_FloatRange(minValue = 0f, maxValue = 1f, stepIncrement = 0.01f, scene = UI_Scene.All)] + UI_FloatRange(minValue = 0f, maxValue = 5f, stepIncrement = 0.1f, scene = UI_Scene.All)] public float evasionTimeThreshold = 0.1f; + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_EvasionNonlinearity", advancedTweakable = true, // Evasion/Extension Nonlinearity + groupName = "pilotAI_EvadeExtend", groupDisplayName = "#LOC_BDArmory_PilotAI_EvadeExtend", groupStartCollapsed = true), + UI_FloatRange(minValue = 0f, maxValue = 10f, stepIncrement = .1f, scene = UI_Scene.All)] + public float evasionNonlinearity = 2f; + float evasionNonlinearityDirection = 1; + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_EvasionIgnoreMyTargetTargetingMe", advancedTweakable = true,//Ignore my target targeting me groupName = "pilotAI_EvadeExtend", groupDisplayName = "#LOC_BDArmory_PilotAI_EvadeExtend", groupStartCollapsed = true), UI_Toggle(enabledText = "#LOC_BDArmory_Enabled", disabledText = "#LOC_BDArmory_Disabled", scene = UI_Scene.All),] @@ -381,6 +504,12 @@ Transform velocityTransform UI_FloatRange(minValue = 0f, maxValue = 2000f, stepIncrement = 10f, scene = UI_Scene.All)] public float extendDistanceAirToAir = 300f; + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_ExtendAngleAirToAir", advancedTweakable = true, //Extend Angle Air-To-Air + groupName = "pilotAI_EvadeExtend", groupDisplayName = "#LOC_BDArmory_PilotAI_EvadeExtend", groupStartCollapsed = true), + UI_FloatRange(minValue = -10f, maxValue = 45f, stepIncrement = 1f, scene = UI_Scene.All)] + public float extendAngleAirToAir = 0f; + float _extendAngleAirToAir = 0; + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_ExtendDistanceAirToGroundGuns", advancedTweakable = true, //Extend Distance Air-To-Ground (Guns) groupName = "pilotAI_EvadeExtend", groupDisplayName = "#LOC_BDArmory_PilotAI_EvadeExtend", groupStartCollapsed = true), UI_FloatRange(minValue = 0f, maxValue = 5000f, stepIncrement = 50f, scene = UI_Scene.All)] @@ -406,6 +535,11 @@ Transform velocityTransform UI_FloatRange(minValue = 0f, maxValue = 5000f, stepIncrement = 25f, scene = UI_Scene.All)] public float extendTargetDist = 300f; + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_ExtendAbortTime", advancedTweakable = true, //Extend Abort Time + groupName = "pilotAI_EvadeExtend", groupDisplayName = "#LOC_BDArmory_PilotAI_EvadeExtend", groupStartCollapsed = true), + UI_FloatRange(minValue = 1f, maxValue = 30f, stepIncrement = 1f, scene = UI_Scene.All)] + public float extendAbortTime = 15f; + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_ExtendToggle", advancedTweakable = true,//Extend Toggle groupName = "pilotAI_EvadeExtend", groupDisplayName = "#LOC_BDArmory_PilotAI_EvadeExtend", groupStartCollapsed = true), UI_Toggle(enabledText = "#LOC_BDArmory_Enabled", disabledText = "#LOC_BDArmory_Disabled", scene = UI_Scene.All),] @@ -422,6 +556,23 @@ Transform velocityTransform groupName = "pilotAI_Terrain", groupDisplayName = "#LOC_BDArmory_PilotAI_Terrain", groupStartCollapsed = true), UI_FloatRange(minValue = 0.1f, maxValue = 5f, stepIncrement = 0.1f, scene = UI_Scene.All)] public float turnRadiusTwiddleFactorMax = 3.0f; // Minimum and maximum twiddle factors for the turn radius. Depends on roll rate and how the vessel behaves under fire. + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_TerrainAvoidanceCriticalAngle", advancedTweakable = true, // Critical angle for inverted terrain avoidance. + groupName = "pilotAI_Terrain", groupDisplayName = "#LOC_BDArmory_PilotAI_Terrain", groupStartCollapsed = true), + UI_FloatRange(minValue = 90f, maxValue = 180f, stepIncrement = 1f, scene = UI_Scene.All)] + public float terrainAvoidanceCriticalAngle = 135f; + float terrainAvoidanceCriticalCosAngle = -0.5f; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_TerrainAvoidanceVesselReactionTime", advancedTweakable = true, // Vessel reaction time. + groupName = "pilotAI_Terrain", groupDisplayName = "#LOC_BDArmory_PilotAI_Terrain", groupStartCollapsed = true), + UI_FloatRange(minValue = 0f, maxValue = 4f, stepIncrement = 0.1f, scene = UI_Scene.All)] + public float controlSurfaceDeploymentTime = 2f; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_WaypointTerrainAvoidance", advancedTweakable = true,//Waypoint terrain avoidance. + groupName = "pilotAI_Terrain", groupDisplayName = "#LOC_BDArmory_PilotAI_Terrain", groupStartCollapsed = true), + UI_FloatRange(minValue = 0f, maxValue = 1f, stepIncrement = 0.01f, scene = UI_Scene.All)] + public float waypointTerrainAvoidance = 0.5f; + float waypointTerrainAvoidanceSmoothingFactor = 0.933f; #endregion #region Ramming @@ -458,26 +609,30 @@ Transform velocityTransform { nameof(steerKiAdjust), 20f }, { nameof(steerDamping), 100f }, { nameof(maxSteer), 1f}, - { nameof(maxSpeed), 3000f }, + { nameof(maxSpeed), (BDArmorySettings.RUNWAY_PROJECT_ROUND == 55) ? 600f : 3000f }, { nameof(takeOffSpeed), 2000f }, { nameof(minSpeed), 2000f }, { nameof(strafingSpeed), 2000f }, { nameof(idleSpeed), 3000f }, { nameof(lowSpeedSwitch), 3000f }, { nameof(cornerSpeed), 3000f }, + { nameof(altitudeSteerLimiterFactor), 10f }, + { nameof(altitudeSteerLimiterAltitude), 100000f }, { nameof(maxAllowedGForce), 1000f }, { nameof(maxAllowedAoA), 180f }, - // { nameof(extendMult), 200f }, + { nameof(postStallAoA), 180f }, { nameof(extendDistanceAirToAir), 20000f }, + { nameof(extendAngleAirToAir), 90f }, { nameof(extendDistanceAirToGroundGuns), 20000f }, { nameof(extendDistanceAirToGround), 20000f }, { nameof(minEvasionTime), 10f }, { nameof(evasionNonlinearity), 90f }, { nameof(evasionThreshold), 300f }, - { nameof(evasionTimeThreshold), 3f }, + { nameof(evasionTimeThreshold), 30f }, { nameof(vesselStandoffDistance), 5000f }, { nameof(turnRadiusTwiddleFactorMin), 10f}, { nameof(turnRadiusTwiddleFactorMax), 10f}, + { nameof(controlSurfaceDeploymentTime), 10f }, { nameof(controlSurfaceLag), 1f}, { nameof(DynamicDampingMin), 100f }, { nameof(DynamicDampingMax), 100f }, @@ -492,16 +647,62 @@ Transform velocityTransform { nameof(DynamicDampingRollMax), 100f }, { nameof(dynamicSteerDampingRollFactor), 100f } }; + Dictionary altMinValues = new Dictionary { + { nameof(extendAngleAirToAir), -90f }, + { nameof(altitudeSteerLimiterFactor), -10f }, + }; + + void TurnItUpToEleven(bool upToEleven) + { + if (pidAutoTuning is not null) + { + // Reset PID values and stop measurement before switching alt values so the correct PID values are used. + pidAutoTuning.RevertPIDValues(); + pidAutoTuning.ResetMeasurements(); + } + using (var s = altMaxValues.Keys.ToList().GetEnumerator()) + while (s.MoveNext()) + { + UI_FloatRange euic = (UI_FloatRange) + (HighLogic.LoadedSceneIsFlight ? Fields[s.Current].uiControlFlight : Fields[s.Current].uiControlEditor); + float tempValue = euic.maxValue; + euic.maxValue = altMaxValues[s.Current]; + altMaxValues[s.Current] = tempValue; + // change the value back to what it is now after fixed update, because changing the max value will clamp it down + // using reflection here, don't look at me like that, this does not run often + StartCoroutine(setVar(s.Current, (float)typeof(BDModulePilotAI).GetField(s.Current).GetValue(this))); + } + using (var s = altMinValues.Keys.ToList().GetEnumerator()) + while (s.MoveNext()) + { + UI_FloatRange euic = (UI_FloatRange) + (HighLogic.LoadedSceneIsFlight ? Fields[s.Current].uiControlFlight : Fields[s.Current].uiControlEditor); + float tempValue = euic.minValue; + euic.minValue = altMinValues[s.Current]; + altMinValues[s.Current] = tempValue; + // change the value back to what it is now after fixed update, because changing the min value will clamp it down + // using reflection here, don't look at me like that, this does not run often + StartCoroutine(setVar(s.Current, (float)typeof(BDModulePilotAI).GetField(s.Current).GetValue(this))); + } + toEleven = upToEleven; + OnAutoTuneOptionsChanged(null, null); // Reset auto-tuning again (including the gradient) so that the correct PID limits are used. + } [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_StandbyMode"),//Standby Mode UI_Toggle(enabledText = "#LOC_BDArmory_On", disabledText = "#LOC_BDArmory_Off")]//On--Off public bool standbyMode = false; + #region Store/Restore private static Dictionary>> storedSettings; // Stored settings for each vessel. [KSPEvent(advancedTweakable = false, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_StoreSettings", active = true)]//Store Settings public void StoreSettings() { - var vesselName = HighLogic.LoadedSceneIsFlight ? vessel.GetDisplayName() : EditorLogic.fetch.ship.shipName; + StoreSettings(null); + } + void StoreSettings(string vesselName) + { + if (vesselName is null) + vesselName = HighLogic.LoadedSceneIsFlight ? vessel.GetDisplayName() : EditorLogic.fetch.ship.shipName; if (storedSettings == null) { storedSettings = new Dictionary>>(); @@ -524,9 +725,15 @@ public void StoreSettings() var fields = typeof(BDModulePilotAI).GetFields(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly); foreach (var field in fields) { + if (field.FieldType == typeof(PIDAutoTuning) || field.FieldType == typeof(Vessel)) // Skip fields that are references to other objects that ought to revert to null. + { + if (BDArmorySettings.DEBUG_AI) Debug.Log($"[BDArmory.BDModulePilotAI]: Skipping {field.Name} of type {field.FieldType} as it's a reference type."); + continue; + } storedSettings[vesselName].Add(new System.Tuple(field.Name, field.GetValue(this))); } Events["RestoreSettings"].active = true; + if (BDArmorySettings.DEBUG_AI) Debug.Log($"[BDArmory.BDModulePilotAI]: Stored AI settings for {vesselName}: " + string.Join(", ", storedSettings[vesselName].Select(s => s.Item1 + "=" + s.Item2))); } [KSPEvent(advancedTweakable = false, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_RestoreSettings", active = false)]//Restore Settings public void RestoreSettings() @@ -545,8 +752,143 @@ public void RestoreSettings() field.SetValue(this, setting.Item2); } } + if (BDArmorySettings.DEBUG_AI) Debug.Log($"[BDArmory.BDModulePilotAI]: Restored AI settings for {vesselName}: " + string.Join(", ", storedSettings[vesselName].Select(s => s.Item1 + "=" + s.Item2))); + } + + // This uses the parts' persistentId to reference the parts. Possibly, it should use some other identifier (what's used as a tag at the end of the "part = ..." and "link = ..." lines?) in case of duplicate persistentIds? + private static Dictionary>>> storedControlSurfaceSettings; // Stored control surface settings for each vessel. + [KSPEvent(advancedTweakable = false, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_StoreControlSurfaceSettings", active = true)]//Store Control Surfaces + public void StoreControlSurfaceSettings() + { + var vesselName = HighLogic.LoadedSceneIsFlight ? vessel.GetDisplayName() : EditorLogic.fetch.ship.shipName; + if (storedControlSurfaceSettings == null) + { + storedControlSurfaceSettings = new Dictionary>>>(); + } + if (storedControlSurfaceSettings.ContainsKey(vesselName)) + { + if (storedControlSurfaceSettings[vesselName] == null) + { + storedControlSurfaceSettings[vesselName] = new Dictionary>>(); + } + else + { + storedControlSurfaceSettings[vesselName].Clear(); + } + } + else + { + storedControlSurfaceSettings.Add(vesselName, new Dictionary>>()); + } + foreach (var part in HighLogic.LoadedSceneIsFlight ? vessel.Parts : EditorLogic.fetch.ship.Parts) + { + var controlSurface = part.GetComponent(); + if (controlSurface == null) continue; + storedControlSurfaceSettings[vesselName][part.persistentId] = new List>(); + var fields = typeof(ModuleControlSurface).GetFields(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly); + foreach (var field in fields) + { + storedControlSurfaceSettings[vesselName][part.persistentId].Add(new System.Tuple(field.Name, field.GetValue(controlSurface))); + } + } + StoreFARControlSurfaceSettings(); + Events["RestoreControlSurfaceSettings"].active = true; + } + private static Dictionary>>> storedFARControlSurfaceSettings; // Stored control surface settings for each vessel. + void StoreFARControlSurfaceSettings() + { + if (!FerramAerospace.hasFARControllableSurface) return; + var vesselName = HighLogic.LoadedSceneIsFlight ? vessel.GetDisplayName() : EditorLogic.fetch.ship.shipName; + if (storedFARControlSurfaceSettings == null) + { + storedFARControlSurfaceSettings = new Dictionary>>>(); + } + if (storedFARControlSurfaceSettings.ContainsKey(vesselName)) + { + if (storedFARControlSurfaceSettings[vesselName] == null) + { + storedFARControlSurfaceSettings[vesselName] = new Dictionary>>(); + } + else + { + storedFARControlSurfaceSettings[vesselName].Clear(); + } + } + else + { + storedFARControlSurfaceSettings.Add(vesselName, new Dictionary>>()); + } + foreach (var part in HighLogic.LoadedSceneIsFlight ? vessel.Parts : EditorLogic.fetch.ship.Parts) + { + foreach (var module in part.Modules) + { + if (module.GetType() == FerramAerospace.FARControllableSurfaceModule) + { + storedFARControlSurfaceSettings[vesselName][part.persistentId] = new List>(); + var fields = FerramAerospace.FARControllableSurfaceModule.GetFields(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly); + foreach (var field in fields) + { + storedFARControlSurfaceSettings[vesselName][part.persistentId].Add(new System.Tuple(field.Name, field.GetValue(module))); + } + break; + } + } + } + } + + [KSPEvent(advancedTweakable = false, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_RestoreControlSurfaceSettings", active = false)]//Restore Control Surfaces + public void RestoreControlSurfaceSettings() + { + RestoreFARControlSurfaceSettings(); + var vesselName = HighLogic.LoadedSceneIsFlight ? vessel.GetDisplayName() : EditorLogic.fetch.ship.shipName; + if (storedControlSurfaceSettings == null || !storedControlSurfaceSettings.ContainsKey(vesselName) || storedControlSurfaceSettings[vesselName] == null || storedControlSurfaceSettings[vesselName].Count == 0) + { + return; + } + foreach (var part in HighLogic.LoadedSceneIsFlight ? vessel.Parts : EditorLogic.fetch.ship.Parts) + { + var controlSurface = part.GetComponent(); + if (controlSurface == null || !storedControlSurfaceSettings[vesselName].ContainsKey(part.persistentId)) continue; + foreach (var setting in storedControlSurfaceSettings[vesselName][part.persistentId]) + { + var field = typeof(ModuleControlSurface).GetField(setting.Item1, BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly); + if (field != null) + { + field.SetValue(controlSurface, setting.Item2); + } + } + } + } + void RestoreFARControlSurfaceSettings() + { + if (!FerramAerospace.hasFARControllableSurface) return; + var vesselName = HighLogic.LoadedSceneIsFlight ? vessel.GetDisplayName() : EditorLogic.fetch.ship.shipName; + if (storedFARControlSurfaceSettings == null || !storedFARControlSurfaceSettings.ContainsKey(vesselName) || storedFARControlSurfaceSettings[vesselName] == null || storedFARControlSurfaceSettings[vesselName].Count == 0) + { + return; + } + foreach (var part in HighLogic.LoadedSceneIsFlight ? vessel.Parts : EditorLogic.fetch.ship.Parts) + { + if (!storedFARControlSurfaceSettings[vesselName].ContainsKey(part.persistentId)) continue; + foreach (var module in part.Modules) + { + if (module.GetType() == FerramAerospace.FARControllableSurfaceModule) + { + foreach (var setting in storedFARControlSurfaceSettings[vesselName][part.persistentId]) + { + var field = FerramAerospace.FARControllableSurfaceModule.GetField(setting.Item1, BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly); + if (field != null) + { + field.SetValue(module, setting.Item2); + } + } + break; + } + } + } } #endregion + #endregion #region AI Internal Parameters bool toEleven = false; @@ -559,29 +901,30 @@ public void RestoreSettings() float dynDecayRate = 1f; // Decay rate for dynamic measurements. Set to a half-life of 60s in Start. float dynVelSmoothingCoef = 1f; // Decay rate for smoothing the dynVelocityMagSqr - float maxAllowedCosAoA; + float maxAllowedSinAoA; float lastAllowedAoA; float maxPosG; - float cosAoAAtMaxPosG; + float sinAoAAtMaxPosG; float maxNegG; - float cosAoAAtMaxNegG; + float sinAoAAtMaxNegG; - float[] gLoadMovingAvgArray = new float[32]; - float[] cosAoAMovingAvgArray = new float[32]; - int movingAvgIndex; + // float[] gLoadMovingAvgArray = new float[32]; + // float[] cosAoAMovingAvgArray = new float[32]; + // int movingAvgIndex; + // float gLoadMovingAvg; + // float cosAoAMovingAvg; + SmoothingF smoothedGLoad; + SmoothingF smoothedSinAoA; - float gLoadMovingAvg; - float cosAoAMovingAvg; - - float gaoASlopePerDynPres; //used to limit control input at very high dynamic pressures to avoid structural failure + float gAoASlopePerDynPres; //used to limit control input at very high dynamic pressures to avoid structural failure float gOffsetPerDynPres; float posPitchDynPresLimitIntegrator = 1; float negPitchDynPresLimitIntegrator = -1; - float lastCosAoA; + float lastSinAoA; float lastPitchInput; //Controller Integral @@ -612,15 +955,15 @@ public float MaxLiftAcceleration float turningTimer; float evasiveTimer; float threatRating; - List waypoints = null; - int activeWaypointIndex = -1; + // List waypoints = null; + // int activeWaypointIndex = -1; Vector3 lastTargetPosition; LineRenderer lr; Vector3 flyingToPosition; Vector3 rollTarget; #if DEBUG - Vector3 DEBUG_vector; + Vector3 debugSquigglySquidDirection; #endif Vector3 angVelRollTarget; @@ -647,12 +990,11 @@ public float MaxLiftAcceleration float terrainAlertDistance; // Distance to the terrain (in the direction of the terrain normal). Vector3 terrainAlertNormal; // Approximate surface normal at the terrain intercept. Vector3 terrainAlertDirection; // Terrain slope in the direction of the velocity at the terrain intercept. - Vector3 terrainAlertCorrectionDirection; // The direction to go to avoid the terrain. - float terrainAlertCoolDown = 0; // Cool down period before allowing other special modes to take effect (currently just "orbitting"). Vector3 relativeVelocityRightDirection; // Right relative to current velocity and upDirection. Vector3 relativeVelocityDownDirection; // Down relative to current velocity and upDirection. - Vector3 terrainAlertDebugPos, terrainAlertDebugDir, terrainAlertDebugPos2, terrainAlertDebugDir2; // Debug vector3's for drawing lines. - bool terrainAlertDebugDraw2 = false; + Vector3 terrainAlertDebugPos, terrainAlertDebugDir; // Debug vector3's for drawing lines. + Color terrainAlertNormalColour = Color.green; // Color of terrain alert normal indicator. + List terrainAlertDebugRays = new List(); // Adjusted normals of terrain alerts used to get the final terrain alert normal. // Ramming public bool ramming = false; // Whether or not we're currently trying to ram someone. @@ -673,9 +1015,8 @@ public float MaxLiftAcceleration Vector3d commandHeading; float finalMaxSteer = 1; - string lastStatus = "Free"; - + bool dirtyPAW_PID = false; // Flag for when the PID part of the PAW needs fixing. #endregion #region RMB info in editor @@ -719,7 +1060,8 @@ public override string GetInfo() #endregion RMB info in editor - protected void SetSliderClamps(string fieldNameMin, string fieldNameMax) + #region UI Initialisers and Callbacks + protected void SetSliderPairClamps(string fieldNameMin, string fieldNameMax) { // Enforce min <= max for pairs of sliders UI_FloatRange field = (UI_FloatRange)Fields[fieldNameMin].uiControlEditor; @@ -750,33 +1092,38 @@ public void OnMaxUpdated(BaseField field, object obj) // if (DynamicDampingRollMin > DynamicDampingRollMax) { DynamicDampingRollMin = DynamicDampingRollMax; } // reversed roll dynamic damp behavior } - void SetAltitudeClamps() + void SetFieldClamps() { var minAltField = (UI_FloatRange)Fields["minAltitude"].uiControlEditor; - minAltField.onFieldChanged = ClampAltitudes; + minAltField.onFieldChanged = ClampFields; minAltField = (UI_FloatRange)Fields["minAltitude"].uiControlFlight; - minAltField.onFieldChanged = ClampAltitudes; + minAltField.onFieldChanged = ClampFields; var defaultAltField = (UI_FloatRange)Fields["defaultAltitude"].uiControlEditor; - defaultAltField.onFieldChanged = ClampAltitudes; + defaultAltField.onFieldChanged = ClampFields; defaultAltField = (UI_FloatRange)Fields["defaultAltitude"].uiControlFlight; - defaultAltField.onFieldChanged = ClampAltitudes; + defaultAltField.onFieldChanged = ClampFields; var maxAltField = (UI_FloatRange)Fields["maxAltitude"].uiControlEditor; - maxAltField.onFieldChanged = ClampAltitudes; + maxAltField.onFieldChanged = ClampFields; maxAltField = (UI_FloatRange)Fields["maxAltitude"].uiControlFlight; - maxAltField.onFieldChanged = ClampAltitudes; + maxAltField.onFieldChanged = ClampFields; + var autoTuningAltField = (UI_FloatRange)Fields["autoTuningAltitude"].uiControlFlight; + autoTuningAltField.onFieldChanged = ClampFields; + var autoTuningSpeedField = (UI_FloatRange)Fields["autoTuningSpeed"].uiControlFlight; + autoTuningSpeedField.onFieldChanged = ClampFields; } - void ClampAltitudes(BaseField field, object obj) + void ClampFields(BaseField field, object obj) { - ClampAltitudes(field.name); + ClampFields(field.name); } - public void ClampAltitudes(string fieldName) + public void ClampFields(string fieldName) { switch (fieldName) { case "minAltitude": if (defaultAltitude < minAltitude) { defaultAltitude = minAltitude; } if (maxAltitude < minAltitude) { maxAltitude = minAltitude; } + UpdateTerrainAlertDetectionRadius(vessel); break; case "defaultAltitude": if (maxAltitude < defaultAltitude) { maxAltitude = defaultAltitude; } @@ -786,8 +1133,14 @@ public void ClampAltitudes(string fieldName) if (minAltitude > maxAltitude) { minAltitude = maxAltitude; } if (defaultAltitude > maxAltitude) { defaultAltitude = maxAltitude; } break; + case "autoTuningAltitude": + autoTuningAltitude = Mathf.Clamp(autoTuningAltitude, 2f * minAltitude, maxAltitude - minAltitude); // Keep the auto-tuning altitude at least minAlt away from the min/max altitudes. + break; + case "autoTuningSpeed": + autoTuningSpeed = Mathf.Clamp(autoTuningSpeed, minSpeed, maxSpeed); // Keep the auto-tuning speed within the combat speed range. + break; default: - Debug.LogError($"[BDArmory.BDModulePilotAI]: Invalid altitude {fieldName} in ClampAltitudes."); + Debug.LogError($"[BDArmory.BDModulePilotAI]: Invalid field name {fieldName} in ClampFields."); break; } } @@ -810,6 +1163,10 @@ public void ToggleDynamicDampingFields() DampingFactor.guiActiveEditor = dynamicSteerDamping && !CustomDynamicAxisFields; // 3-axis dynamic damping + var CustomDynamicAxisToggleField = Fields["CustomDynamicAxisFields"]; + CustomDynamicAxisToggleField.guiActive = dynamicSteerDamping; + CustomDynamicAxisToggleField.guiActiveEditor = dynamicSteerDamping; + var DynamicPitchLabel = Fields["PitchLabel"]; var DynamicDampingPitch = Fields["dynamicDampingPitch"]; var DynamicDampingPitchMaxField = Fields["DynamicDampingPitchMax"]; @@ -861,23 +1218,11 @@ public void ToggleDynamicDampingFields() DynamicDampingRollFactorField.guiActive = CustomDynamicAxisFields && dynamicSteerDamping; DynamicDampingRollFactorField.guiActiveEditor = CustomDynamicAxisFields && dynamicSteerDamping; - StartCoroutine(ToggleDynamicDampingButtons()); - } - - IEnumerator ToggleDynamicDampingButtons() - { - // Toggle the visibility of buttons, then re-enable them to avoid messing up the order in the GUI. - var dynamicSteerDampingField = Fields["dynamicSteerDamping"]; - var customDynamicAxisField = Fields["CustomDynamicAxisFields"]; - dynamicSteerDampingField.guiActive = false; - dynamicSteerDampingField.guiActiveEditor = false; - customDynamicAxisField.guiActive = false; - customDynamicAxisField.guiActiveEditor = false; - yield return new WaitForFixedUpdate(); - dynamicSteerDampingField.guiActive = true; - dynamicSteerDampingField.guiActiveEditor = true; - customDynamicAxisField.guiActive = dynamicDamping; - customDynamicAxisField.guiActiveEditor = dynamicDamping; + dirtyPAW_PID = true; + if (HighLogic.LoadedSceneIsFlight && autoTune) // Disable auto-tuning if the damping configuration is changed. + { + AutoTune = false; + } } [KSPAction("Toggle Max Altitude (AGL)")] @@ -915,6 +1260,45 @@ void SetMinCollisionAvoidanceLookAheadPeriod() minCollisionAvoidanceLookAheadPeriod.minValue = vesselCollisionAvoidanceTickerFreq * Time.fixedDeltaTime; } + public void SetOnExtendAngleA2AChanged() + { + UI_FloatRange field = (UI_FloatRange)Fields["extendAngleAirToAir"].uiControlEditor; + field.onFieldChanged = OnExtendAngleA2AChanged; + field = (UI_FloatRange)Fields["extendAngleAirToAir"].uiControlFlight; + field.onFieldChanged = OnExtendAngleA2AChanged; + OnExtendAngleA2AChanged(null, null); + } + void OnExtendAngleA2AChanged(BaseField field, object obj) + { + _extendAngleAirToAir = Mathf.Sin(extendAngleAirToAir * Mathf.Deg2Rad); + } + + public void SetOnTerrainAvoidanceCriticalAngleChanged() + { + UI_FloatRange field = (UI_FloatRange)Fields["terrainAvoidanceCriticalAngle"].uiControlEditor; + field.onFieldChanged = OnTerrainAvoidanceCriticalAngleChanged; + field = (UI_FloatRange)Fields["terrainAvoidanceCriticalAngle"].uiControlFlight; + field.onFieldChanged = OnTerrainAvoidanceCriticalAngleChanged; + OnTerrainAvoidanceCriticalAngleChanged(null, null); + } + public void OnTerrainAvoidanceCriticalAngleChanged(BaseField field, object obj) + { + terrainAvoidanceCriticalCosAngle = Mathf.Cos(terrainAvoidanceCriticalAngle * Mathf.Deg2Rad); + } + + public void SetOnImmelmannTurnAngleChanged() + { + UI_FloatRange field = (UI_FloatRange)Fields["ImmelmannTurnAngle"].uiControlEditor; + field.onFieldChanged = OnImmelmannTurnAngleChanged; + field = (UI_FloatRange)Fields["ImmelmannTurnAngle"].uiControlFlight; + field.onFieldChanged = OnImmelmannTurnAngleChanged; + OnImmelmannTurnAngleChanged(null, null); + } + public void OnImmelmannTurnAngleChanged(BaseField field, object obj) + { + ImmelmannTurnCosAngle = -Mathf.Cos(ImmelmannTurnAngle * Mathf.Deg2Rad); + } + IEnumerator FixAltitudesSectionLayout() // Fix the layout of the Altitudes section by briefly disabling the fields underneath the one that was removed. { var maxAltitudeToggleField = Fields["maxAltitudeToggle"]; @@ -952,15 +1336,14 @@ void OnSliderResolutionUpdated(BaseField field, object obj) { if (PIDField.group.name == "pilotAI_PID") { + if (PIDField.name.StartsWith("autoTuning")) continue; var uiControl = HighLogic.LoadedSceneIsFlight ? PIDField.uiControlFlight : PIDField.uiControlEditor; if (uiControl.GetType() == typeof(UI_FloatRange)) { var slider = (UI_FloatRange)uiControl; var alsoMinValue = (slider.minValue == slider.stepIncrement); slider.stepIncrement *= factor; - slider.stepIncrement = Utils.RoundToUnit(slider.stepIncrement, slider.stepIncrement); - // var precision = Mathf.Pow(10, -Mathf.Floor(Mathf.Log10(slider.stepIncrement)) + 1); - // slider.stepIncrement = Mathf.Round(precision * slider.stepIncrement) / precision; + slider.stepIncrement = BDAMath.RoundToUnit(slider.stepIncrement, slider.stepIncrement); if (alsoMinValue) slider.minValue = slider.stepIncrement; } } @@ -972,9 +1355,7 @@ void OnSliderResolutionUpdated(BaseField field, object obj) var slider = (UI_FloatRange)uiControl; var alsoMinValue = (slider.minValue == slider.stepIncrement); slider.stepIncrement *= factor; - slider.stepIncrement = Utils.RoundToUnit(slider.stepIncrement, slider.stepIncrement); - // var precision = Mathf.Pow(10, -Mathf.Floor(Mathf.Log10(slider.stepIncrement)) + 1); - // slider.stepIncrement = Mathf.Round(precision * slider.stepIncrement) / precision; + slider.stepIncrement = BDAMath.RoundToUnit(slider.stepIncrement, slider.stepIncrement); if (alsoMinValue) slider.minValue = slider.stepIncrement; } } @@ -985,9 +1366,7 @@ void OnSliderResolutionUpdated(BaseField field, object obj) { var slider = (UI_FloatRange)uiControl; slider.stepIncrement *= factor; - slider.stepIncrement = Utils.RoundToUnit(slider.stepIncrement, slider.stepIncrement); - // var precision = Mathf.Pow(10, -Mathf.Floor(Mathf.Log10(slider.stepIncrement)) + 1); - // slider.stepIncrement = Mathf.Round(precision * slider.stepIncrement) / precision; + slider.stepIncrement = BDAMath.RoundToUnit(slider.stepIncrement, slider.stepIncrement); } } if (PIDField.group.name == "pilotAI_EvadeExtend") @@ -999,9 +1378,7 @@ void OnSliderResolutionUpdated(BaseField field, object obj) { var slider = (UI_FloatRange)uiControl; slider.stepIncrement *= factor; - slider.stepIncrement = Utils.RoundToUnit(slider.stepIncrement, slider.stepIncrement); - // var precision = Mathf.Pow(10, -Mathf.Floor(Mathf.Log10(slider.stepIncrement)) + 1); - // slider.stepIncrement = Mathf.Round(precision * slider.stepIncrement) / precision; + slider.stepIncrement = BDAMath.RoundToUnit(slider.stepIncrement, slider.stepIncrement); } } } @@ -1010,42 +1387,233 @@ void OnSliderResolutionUpdated(BaseField field, object obj) } } + void SetupAutoTuneSliders() + { + if (HighLogic.LoadedSceneIsEditor) + { + UI_Toggle autoTuneToggle = (UI_Toggle)Fields["autoTune"].uiControlEditor; + autoTuneToggle.onFieldChanged = OnAutoTuneChanged; + } + else if (HighLogic.LoadedSceneIsFlight) + { + pidAutoTuning = new PIDAutoTuning(this); + UI_Toggle autoTuneToggle = (UI_Toggle)Fields["autoTune"].uiControlFlight; + autoTuneToggle.onFieldChanged = OnAutoTuneChanged; + foreach (var field in Fields) + { + var fieldName = field.name; + if (!fieldName.StartsWith("autoTuningOption")) continue; + if (fieldName.StartsWith("autoTuningOptionFixed")) continue; + if (Fields.TryGetFieldUIControl(fieldName, out UI_Control autoTuneField)) + { + autoTuneField.onFieldChanged = OnAutoTuneOptionsChanged; + } + } + } + SetAutoTuneFields(); + } + public void OnAutoTuneChanged(BaseField field, object obj) + { + if (HighLogic.LoadedSceneIsEditor) SetAutoTuneFields(); + if (!HighLogic.LoadedSceneIsFlight) return; + if (!autoTune) + { + pidAutoTuning.RevertPIDValues(); + StoreSettings(pidAutoTuning.vesselName); // Store the current settings for recall in the SPH. + } + pidAutoTuning.SetStartCoords(); + pidAutoTuning.ResetMeasurements(); + + SetAutoTuneFields(); + CheatOptions.InfinitePropellant = autoTune || BDArmorySettings.INFINITE_FUEL; // Prevent fuel drain while auto-tuning. + OtherUtils.SetTimeOverride(autoTune); + } + void SetAutoTuneFields() + { + if (!(HighLogic.LoadedSceneIsEditor || HighLogic.LoadedSceneIsFlight)) return; + if (HighLogic.LoadedSceneIsEditor) + { + foreach (var field in Fields) + { + if (field.name.StartsWith("autoTuningOptionFixed")) continue; + if (field.name.StartsWith("autoTuning")) + { + field.guiActiveEditor = autoTune; + } + } + } + else + { + foreach (var field in Fields) + { + if (field.name.StartsWith("autoTuningOptionFixed")) continue; + if (field.name.StartsWith("autoTuning")) + { + field.guiActive = autoTune; + } + } + } + dirtyPAW_PID = true; + } + void OnAutoTuneOptionsChanged(BaseField field, object obj) + { + if (pidAutoTuning is null) return; + pidAutoTuning.RevertPIDValues(); + pidAutoTuning.ResetMeasurements(); + pidAutoTuning.ResetGradient(); + } + + bool fixFieldOrderingRunning = false; + /// + /// Fix the field ordering in the PAW due to setting fields active or inactive. + /// + /// + /// + IEnumerator FixFieldOrdering(string groupName, string startFieldName = null) + { + if (fixFieldOrderingRunning || !(HighLogic.LoadedSceneIsEditor || HighLogic.LoadedSceneIsFlight)) yield break; + fixFieldOrderingRunning = true; + Dictionary fieldStates = new Dictionary(); + bool foundStartField = (startFieldName is null); + foreach (var field in Fields) + { + if (field.group.name != groupName) continue; + if (!foundStartField && field.name != startFieldName) continue; + foundStartField = true; + if (HighLogic.LoadedSceneIsEditor) + { + fieldStates.Add(field.name, field.guiActiveEditor); + field.guiActiveEditor = false; + } + else + { + fieldStates.Add(field.name, field.guiActive); + field.guiActive = false; + } + } + yield return null; + foreach (var field in Fields) + { + if (fieldStates.ContainsKey(field.name)) + { + if (HighLogic.LoadedSceneIsEditor) + field.guiActiveEditor = fieldStates[field.name]; + else + field.guiActive = fieldStates[field.name]; + } + } + dirtyPAW_PID = false; + fixFieldOrderingRunning = false; + } + + void PAWFirstOpened(UIPartActionWindow paw, Part p) // Fix the ordering of fields when the PAW is first opened. This is required since KSP messes up the field ordering if the first KSPField is in a collapsed group. + { + if (p != part) return; + dirtyPAW_PID = true; + GameEvents.onPartActionUIShown.Remove(PAWFirstOpened); + } + #endregion + protected override void Start() { base.Start(); if (HighLogic.LoadedSceneIsFlight) { - maxAllowedCosAoA = (float)Math.Cos(maxAllowedAoA * Math.PI / 180.0); + maxAllowedSinAoA = (float)Math.Sin(maxAllowedAoA * Mathf.Deg2Rad); lastAllowedAoA = maxAllowedAoA; GameEvents.onVesselPartCountChanged.Add(UpdateTerrainAlertDetectionRadius); UpdateTerrainAlertDetectionRadius(vessel); dynDecayRate = Mathf.Exp(Mathf.Log(0.5f) * Time.fixedDeltaTime / 60f); // Decay rate for a half-life of 60s. - dynVelSmoothingCoef = Mathf.Exp(Mathf.Log(0.5f) * Time.fixedDeltaTime / 5f); // Smoothing rate with a half-life of 5s. + dynVelSmoothingCoef = Mathf.Exp(Mathf.Log(0.5f) * Time.fixedDeltaTime); // Smoothing rate with a half-life of 1s. + smoothedGLoad = new SmoothingF(Mathf.Exp(Mathf.Log(0.5f) * Time.fixedDeltaTime * 10f)); // Half-life of 0.1s. + smoothedSinAoA = new SmoothingF(Mathf.Exp(Mathf.Log(0.5f) * Time.fixedDeltaTime * 10f)); // Half-life of 0.1s. + } + if (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 55) + { + maxBank = Mathf.Min(maxBank, 40); + postStallAoA = 0.0f; + maxSpeed = Mathf.Min(maxSpeed, 600); + if (HighLogic.LoadedSceneIsFlight) + { + UI_FloatRange bank = (UI_FloatRange)Fields["maxBank"].uiControlFlight; + bank.maxValue = 40; + UI_FloatRange spd = (UI_FloatRange)Fields["maxSpeed"].uiControlFlight; + spd.maxValue = 600; + } + else + { + UI_FloatRange bank = (UI_FloatRange)Fields["maxBank"].uiControlEditor; + bank.maxValue = 40; + UI_FloatRange spd = (UI_FloatRange)Fields["maxSpeed"].uiControlEditor; + spd.maxValue = 600; + } + Fields["postStallAoA"].guiActiveEditor = false; + Fields["postStallAoA"].guiActive = false; + } + if (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 60) + { + minAltitude = Mathf.Max(minAltitude, 750); + UI_FloatRange minAlt = (UI_FloatRange)Fields["minAltitude"].uiControlFlight; + minAlt.minValue = 750; + defaultAltitude = BDArmorySettings.VESSEL_SPAWN_ALTITUDE; + Fields["defaultAltitude"].guiActiveEditor = false; + Fields["defaultAltitude"].guiActive = false; + maxAllowedAoA = 2.5f; + postStallAoA = 5; + maxSpeed = Mathf.Min(250, maxSpeed); + UI_FloatRange spd = (UI_FloatRange)Fields["maxSpeed"].uiControlFlight; + spd.maxValue = 250; + Fields["postStallAoA"].guiActiveEditor = false; + Fields["postStallAoA"].guiActive = false; + Fields["maxAllowedAoA"].guiActiveEditor = false; + Fields["maxAllowedAoA"].guiActive = false; } - SetupSliderResolution(); - SetSliderClamps("turnRadiusTwiddleFactorMin", "turnRadiusTwiddleFactorMax"); + SetSliderPairClamps("turnRadiusTwiddleFactorMin", "turnRadiusTwiddleFactorMax"); // SetSliderClamps("DynamicDampingMin", "DynamicDampingMax"); // SetSliderClamps("DynamicDampingPitchMin", "DynamicDampingPitchMax"); // SetSliderClamps("DynamicDampingYawMin", "DynamicDampingYawMax"); // SetSliderClamps("DynamicDampingRollMin", "DynamicDampingRollMax"); - SetAltitudeClamps(); + SetFieldClamps(); SetMinCollisionAvoidanceLookAheadPeriod(); + SetWaypointTerrainAvoidance(); dynamicDamping = dynamicSteerDamping; CustomDynamicAxisField = CustomDynamicAxisFields; ToggleDynamicDampingFields(); ToggleMaxAltitude(); - // InitSteerDamping(); + SetOnExtendAngleA2AChanged(); + SetOnTerrainAvoidanceCriticalAngleChanged(); + SetOnImmelmannTurnAngleChanged(); + SetupAutoTuneSliders(); if ((HighLogic.LoadedSceneIsFlight || HighLogic.LoadedSceneIsEditor) && storedSettings != null && storedSettings.ContainsKey(HighLogic.LoadedSceneIsFlight ? vessel.GetDisplayName() : EditorLogic.fetch.ship.shipName)) { Events["RestoreSettings"].active = true; } - } - + if (HighLogic.LoadedSceneIsFlight || HighLogic.LoadedSceneIsEditor) + { + var vesselName = HighLogic.LoadedSceneIsFlight ? vessel.GetDisplayName() : EditorLogic.fetch.ship.shipName; + if ((storedControlSurfaceSettings != null && storedControlSurfaceSettings.ContainsKey(vesselName)) || (storedFARControlSurfaceSettings != null && storedFARControlSurfaceSettings.ContainsKey(vesselName))) + { + Events["RestoreControlSurfaceSettings"].active = true; + } + } + GameEvents.onPartActionUIShown.Add(PAWFirstOpened); + } + protected override void OnDestroy() { + GameEvents.onPartActionUIShown.Remove(PAWFirstOpened); GameEvents.onVesselPartCountChanged.Remove(UpdateTerrainAlertDetectionRadius); + if (autoTune) + { + if (pidAutoTuning is not null) // If we were auto-tuning, revert to the best values and store them. + { + pidAutoTuning.RevertPIDValues(); + StoreSettings(pidAutoTuning.vesselName); + } + OtherUtils.SetTimeOverride(false); // Make sure we disable the Time Override if we were auto-tuning. + } base.OnDestroy(); } @@ -1063,55 +1631,34 @@ public override void ActivatePilot() void Update() { - if (BDArmorySettings.DRAW_DEBUG_LINES && pilotEnabled) + if (BDArmorySettings.DEBUG_LINES && pilotEnabled) { - if (lr) - { - lr.enabled = true; - lr.SetPosition(0, vessel.ReferenceTransform.position); - lr.SetPosition(1, flyingToPosition); - } - else + lr = GetComponent(); + if (lr == null) { lr = gameObject.AddComponent(); lr.positionCount = 2; lr.startWidth = 0.5f; lr.endWidth = 0.5f; } + lr.enabled = true; + lr.SetPosition(0, vessel.ReferenceTransform.position); + lr.SetPosition(1, flyingToPosition); minSpeed = Mathf.Clamp(minSpeed, 0, idleSpeed - 20); minSpeed = Mathf.Clamp(minSpeed, 0, maxSpeed - 20); } - else - { - if (lr) - { - lr.enabled = false; - } - } + else { if (lr != null) { lr.enabled = false; } } // switch up the alt values if up to eleven is toggled if (UpToEleven != toEleven) { - using (var s = altMaxValues.Keys.ToList().GetEnumerator()) - while (s.MoveNext()) - { - UI_FloatRange euic = (UI_FloatRange) - (HighLogic.LoadedSceneIsFlight ? Fields[s.Current].uiControlFlight : Fields[s.Current].uiControlEditor); - float tempValue = euic.maxValue; - euic.maxValue = altMaxValues[s.Current]; - altMaxValues[s.Current] = tempValue; - // change the value back to what it is now after fixed update, because changing the max value will clamp it down - // using reflection here, don't look at me like that, this does not run often - StartCoroutine(setVar(s.Current, (float)typeof(BDModulePilotAI).GetField(s.Current).GetValue(this))); - } - toEleven = UpToEleven; + TurnItUpToEleven(UpToEleven); } //hide dynamic steer damping fields if dynamic damping isn't toggled if (dynamicSteerDamping != dynamicDamping) { - // InitSteerDamping(); dynamicDamping = dynamicSteerDamping; ToggleDynamicDampingFields(); } @@ -1127,6 +1674,8 @@ void Update() { ToggleMaxAltitude(); } + + if (dirtyPAW_PID) StartCoroutine(FixFieldOrdering("pilotAI_PID")); } IEnumerator setVar(string name, float value) @@ -1134,13 +1683,33 @@ IEnumerator setVar(string name, float value) yield return new WaitForFixedUpdate(); typeof(BDModulePilotAI).GetField(name).SetValue(this, value); } - + float targetStalenessTimer = 0; void FixedUpdate() { //floating origin and velocity offloading corrections - if (!FloatingOrigin.Offset.IsZero() || !Krakensbane.GetFrameVelocity().IsZero()) + if (!HighLogic.LoadedSceneIsFlight) return; + if (BDKrakensbane.IsActive) { - if (lastTargetPosition != null) lastTargetPosition -= FloatingOrigin.OffsetNonKrakensbane; + if (lastTargetPosition != null) lastTargetPosition -= BDKrakensbane.FloatingOriginOffsetNonKrakensbane; + } + if (weaponManager && weaponManager.guardMode && weaponManager.staleTarget) + { + targetStalenessTimer += Time.fixedDeltaTime; + if (targetStalenessTimer >= 1) //add some error to the predicted position every second + { + /* + staleTargetPosition = new Vector3(); + staleTargetPosition.x = UnityEngine.Random.Range(-(float)staleTargetVelocity.magnitude / 2, (float)staleTargetVelocity.magnitude / 2); + staleTargetPosition.y = UnityEngine.Random.Range(-(float)staleTargetVelocity.magnitude / 2, (float)staleTargetVelocity.magnitude / 2); + staleTargetPosition.z = UnityEngine.Random.Range(-(float)staleTargetVelocity.magnitude / 2, (float)staleTargetVelocity.magnitude / 2); + */ + staleTargetPosition = UnityEngine.Random.insideUnitSphere * staleTargetVelocity.magnitude / 2; + targetStalenessTimer = 0; + } + } + else + { + if (targetStalenessTimer != 0) targetStalenessTimer = 0; } } @@ -1149,9 +1718,6 @@ protected override void AutoPilot(FlightCtrlState s) { finalMaxSteer = 1f; // Reset finalMaxSteer, is adjusted in subsequent methods - if (terrainAlertCoolDown > 0) - terrainAlertCoolDown -= Time.fixedDeltaTime; - //default brakes off full throttle //s.mainThrottle = 1; @@ -1165,7 +1731,7 @@ protected override void AutoPilot(FlightCtrlState s) vessel.ActionGroups.SetGroup(KSPActionGroup.RCS, true); } - steerMode = SteerModes.NormalFlight; + if (!ramming) steerMode = SteerModes.NormalFlight; // Reset the steer mode, unless we're ramming. useVelRollTarget = false; // landed and still, chill out @@ -1177,27 +1743,27 @@ protected override void AutoPilot(FlightCtrlState s) return; } - //upDirection = -FlightGlobals.getGeeForceAtPosition(transform.position).normalized; upDirection = VectorUtils.GetUpDirection(vessel.transform.position); CalculateAccelerationAndTurningCircle(); + CheckFlatSpin(); if ((float)vessel.radarAltitude < minAltitude) { belowMinAltitude = true; } - if (gainAltInhibited && (!belowMinAltitude || !(currentStatus == "Engaging" || currentStatus == "Evading" || currentStatus.StartsWith("Gain Alt")))) - { // Allow switching between "Engaging", "Evading" and "Gain Alt." while below minimum altitude without disabling the gain altitude inhibitor. + if (gainAltInhibited && (!belowMinAltitude || !(currentStatus == "Engaging" || currentStatus == "Evading" || currentStatus == "Ramming speed!" || currentStatus.StartsWith("Gain Alt")))) + { // Allow switching between "Engaging", "Evading", "Ramming speed!" and "Gain Alt." while below minimum altitude without disabling the gain altitude inhibitor. gainAltInhibited = false; - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BDModulePilotAI]: " + vessel.vesselName + " is no longer inhibiting gain alt"); + if (BDArmorySettings.DEBUG_AI) Debug.Log("[BDArmory.BDModulePilotAI]: " + vessel.vesselName + " is no longer inhibiting gain alt"); } - if (!gainAltInhibited && belowMinAltitude && (currentStatus == "Engaging" || currentStatus == "Evading")) - { // Vessel went below minimum altitude while "Engaging" or "Evading", enable the gain altitude inhibitor. + if (!gainAltInhibited && belowMinAltitude && (currentStatus == "Engaging" || currentStatus == "Evading" || currentStatus == "Ramming speed!") && vessel.atmDensity > 0.1f) + { // Vessel went below minimum altitude while "Engaging", "Evading" or "Ramming speed!", enable the gain altitude inhibitor. gainAltInhibited = true; - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BDModulePilotAI]: " + vessel.vesselName + " was " + currentStatus + " and went below min altitude, inhibiting gain alt."); + if (BDArmorySettings.DEBUG_AI) Debug.Log("[BDArmory.BDModulePilotAI]: " + vessel.vesselName + " was " + currentStatus + " and went below min altitude, inhibiting gain alt."); } - if (vessel.srfSpeed < minSpeed) + if ((vessel.srfSpeed < minSpeed) || (FlatSpin != 0)) { regainEnergy = true; } else if (!belowMinAltitude && vessel.srfSpeed > Mathf.Min(minSpeed + 20f, idleSpeed)) { regainEnergy = false; } @@ -1205,25 +1771,28 @@ protected override void AutoPilot(FlightCtrlState s) UpdateVelocityRelativeDirections(); CheckLandingGear(); - if (IsFlyingWaypoints) UpdateWaypoint(); // Update the waypoint state. + if (IsRunningWaypoints) UpdateWaypoint(); // Update the waypoint state. - if (!vessel.LandedOrSplashed && (FlyAvoidTerrain(s) || (!ramming && FlyAvoidOthers(s)))) + if (!vessel.LandedOrSplashed && ((!(ramming && steerMode == SteerModes.Aiming) && FlyAvoidTerrain(s)) || (!ramming && FlyAvoidOthers(s)))) // Avoid terrain and other planes, unless we're trying to ram stuff. { turningTimer = 0; } - else if (belowMinAltitude && !(gainAltInhibited || BDArmorySettings.SF_REPULSOR)) // If we're below minimum altitude, gain altitude unless we're being inhibited or the space friction repulsor field is enabled. + else if (initialTakeOff) // Take off. { - if (initialTakeOff || command != PilotCommands.Follow) - { - TakeOff(s); - turningTimer = 0; - } - else // Have taken off, but is in Follow mode. - { UpdateCommand(s); } + TakeOff(s); + turningTimer = 0; } else { - if (command != PilotCommands.Free && command != PilotCommands.Waypoints) - { UpdateCommand(s); } - else + if (!(command == PilotCommands.Free || command == PilotCommands.Waypoints)) + { + if (belowMinAltitude && !(gainAltInhibited || BDArmorySettings.SF_REPULSOR)) // If we're below minimum altitude, gain altitude unless we're being inhibited or the space friction repulsor field is enabled. + { + TakeOff(s); + turningTimer = 0; + } + else // Follow the current command. + { UpdateCommand(s); } + } + else // Do combat stuff or orbit. (minAlt is handled in UpdateAI for Free and Waypoints modes.) { UpdateAI(s); } } UpdateGAndAoALimits(s); @@ -1232,11 +1801,10 @@ protected override void AutoPilot(FlightCtrlState s) // Perform the check here since we're now allowing evading/engaging while below mininum altitude. if (belowMinAltitude && vessel.radarAltitude > minAltitude && Vector3.Dot(vessel.Velocity(), vessel.upAxis) > 0) // We're good. { - terrainAlertCoolDown = 1.0f; // 1s cool down after avoiding terrain or gaining altitude. (Only used for delaying "orbitting" for now.) belowMinAltitude = false; } - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_AI) { if (lastStatus != currentStatus && !(lastStatus.StartsWith("Gain Alt.") && currentStatus.StartsWith("Gain Alt.")) && !(lastStatus.StartsWith("Terrain") && currentStatus.StartsWith("Terrain")) && !(lastStatus.StartsWith("Waypoint") && currentStatus.StartsWith("Waypoint"))) { @@ -1257,11 +1825,16 @@ void UpdateAI(FlightCtrlState s) threatRating = evasionThreshold + 1f; // Don't evade by default wasEvading = evading; evading = false; + if (extendAbortTimer < 0) // Extending is in cooldown. + { + extendAbortTimer += TimeWarp.fixedDeltaTime; + if (extendAbortTimer > 0) extendAbortTimer = 0; + } if (weaponManager != null) { if (weaponManager.incomingMissileTime <= weaponManager.cmThreshold) { - threatRating = 0f; // Allow entering evasion code if we're under missile fire + threatRating = -1f; // Allow entering evasion code if we're under missile fire minimumEvasionTime = 0f; // Trying to evade missile threats when they don't exist will result in NREs } else if (weaponManager.underFire && !ramming) // If we're ramming, ignore gunfire. @@ -1271,7 +1844,7 @@ void UpdateAI(FlightCtrlState s) } } - debugString.AppendLine($"Threat Rating: {threatRating}"); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) debugString.AppendLine($"Threat Rating: {threatRating:G3}"); // If we're currently evading or a threat is significant and we're not ramming. if ((evasiveTimer < minimumEvasionTime && evasiveTimer != 0) || threatRating < evasionThreshold) @@ -1282,31 +1855,11 @@ void UpdateAI(FlightCtrlState s) if (weaponManager) { - if (weaponManager.rwr != null ? weaponManager.rwr.rwrEnabled : false) //use rwr to check missile threat direction + if (weaponManager.incomingMissileVessel)//switch to weaponManager.missileisIncoming? { - Vector3 missileThreat = Vector3.zero; - bool missileThreatDetected = false; - float closestMissileThreat = float.MaxValue; - for (int i = 0; i < weaponManager.rwr.pingsData.Length; i++) - { - TargetSignatureData threat = weaponManager.rwr.pingsData[i]; - if (threat.exists && threat.signalStrength == (float)RadarWarningReceiver.RWRThreatTypes.MissileLock) - { - missileThreatDetected = true; - float dist = (weaponManager.rwr.pingWorldPositions[i] - vesselTransform.position).sqrMagnitude; - if (dist < closestMissileThreat) - { - closestMissileThreat = dist; - missileThreat = weaponManager.rwr.pingWorldPositions[i]; - } - } - } - if (missileThreatDetected) - { - threatRelativePosition = missileThreat - vesselTransform.position; - if (extending) - StopExtending("missile threat"); // Don't keep trying to extend if under fire from missiles - } + threatRelativePosition = weaponManager.incomingThreatPosition - vesselTransform.position; + if (extending) + StopExtending("missile threat"); // Don't keep trying to extend if under fire from missiles } if (weaponManager.underFire) @@ -1324,11 +1877,19 @@ void UpdateAI(FlightCtrlState s) evasiveTimer = 0; collisionDetectionTicker = vesselCollisionAvoidanceTickerFreq + 1; //check for collision again after exiting evasion routine } + if (evading) return; } - else if (!extending && IsFlyingWaypoints) + else if (belowMinAltitude && !(gainAltInhibited || BDArmorySettings.SF_REPULSOR)) // If we're below minimum altitude, gain altitude unless we're being inhibited or the space friction repulsor field is enabled. + { + TakeOff(s); // Gain Altitude + turningTimer = 0; + return; + } + else if (!extending && IsRunningWaypoints) { // FIXME To avoid getting stuck circling a waypoint, a check should be made (maybe use the turningTimer for this?), in which case the plane should RequestExtend away from the waypoint. FlyWaypoints(s); + return; } else if (!extending && weaponManager && targetVessel != null && targetVessel.transform != null) { @@ -1336,7 +1897,7 @@ void UpdateAI(FlightCtrlState s) if (!targetVessel.LandedOrSplashed) { Vector3 targetVesselRelPos = targetVessel.vesselTransform.position - vesselTransform.position; - if (canExtend && vessel.altitude < defaultAltitude && Vector3.Angle(targetVesselRelPos, -upDirection) < 35) // Target is at a steep angle below us and we're below default altitude, extend to get a better angle instead of attacking now. + if (canExtend && vessel.radarAltitude < defaultAltitude && Vector3.Angle(targetVesselRelPos, -upDirection) < 35) // Target is at a steep angle below us and we're below default altitude, extend to get a better angle instead of attacking now. { RequestExtend("too steeply below", targetVessel); } @@ -1350,7 +1911,7 @@ void UpdateAI(FlightCtrlState s) turningTimer = 0; } - debugString.AppendLine($"turningTimer: {turningTimer}"); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) debugString.AppendLine($"turningTimer: {turningTimer}"); float targetForwardDot = Vector3.Dot(targetVesselRelPos.normalized, vesselTransform.up); // Cosine of angle between us and target (1 if target is in front of us , -1 if target is behind us) float targetVelFrac = (float)(targetVessel.srfSpeed / vessel.srfSpeed); //this is the ratio of the target vessel's velocity to this vessel's srfSpeed in the forward direction; this allows smart decisions about when to break off the attack @@ -1379,18 +1940,25 @@ void UpdateAI(FlightCtrlState s) { ramming = false; SetStatus("Engaging"); - debugString.AppendLine($"Flying to target " + targetVessel.vesselName); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) debugString.AppendLine($"Flying to target " + targetVessel.vesselName); FlyToTargetVessel(s, targetVessel); + return; } } } else { evasiveTimer = 0; - if (!extending && !(terrainAlertCoolDown > 0)) + if (!extending) { + if (ResumeCommand()) + { + UpdateCommand(s); + return; + } SetStatus("Orbiting"); FlyOrbit(s, assignedPositionGeo, 2000, idleSpeed, ClockwiseOrbit); + return; } } @@ -1399,8 +1967,8 @@ void UpdateAI(FlightCtrlState s) weaponManager.ForceScan(); evasiveTimer = 0; SetStatus("Extending"); - debugString.AppendLine($"Extending"); FlyExtend(s, lastTargetPosition); + return; } } @@ -1414,7 +1982,7 @@ bool PredictCollisionWithVessel(Vessel v, float maxTime, out Vector3 badDirectio } // Adjust some values for asteroids. - var targetRadius = v.GetRadius(true); + var targetRadius = v.GetRadius(); var threshold = collisionAvoidanceThreshold + targetRadius; // Add the target's average radius to the threshold. if (v.vesselType == VesselType.SpaceObject) // Give asteroids some extra room. { @@ -1422,7 +1990,7 @@ bool PredictCollisionWithVessel(Vessel v, float maxTime, out Vector3 badDirectio } // Use the nearest time to closest point of approach to check separation instead of iteratively sampling. Should give faster, more accurate results. - float timeToCPA = vessel.ClosestTimeToCPA(v, maxTime); // This uses the same kinematics as AIUtils.PredictPosition. + float timeToCPA = vessel.TimeToCPA(v, maxTime); // This uses the same kinematics as AIUtils.PredictPosition. if (timeToCPA > 0 && timeToCPA < maxTime) { Vector3 tPos = AIUtils.PredictPosition(v, timeToCPA); @@ -1446,7 +2014,7 @@ bool RamTarget(FlightCtrlState s, Vessel v) Vector3 relVelocity = v.Velocity() - vessel.Velocity(); Vector3 relPosition = v.transform.position - vessel.transform.position; Vector3 relAcceleration = v.acceleration - vessel.acceleration; - float timeToCPA = vessel.ClosestTimeToCPA(v, 16f); + float timeToCPA = vessel.TimeToCPA(v, 16f); // Let's try to ram someone! if (!ramming) @@ -1472,18 +2040,22 @@ bool RamTarget(FlightCtrlState s, Vessel v) return true; } + Vector3 staleTargetPosition = Vector3.zero; + Vector3 staleTargetVelocity = Vector3.zero; void FlyToTargetVessel(FlightCtrlState s, Vessel v) { Vector3 target = AIUtils.PredictPosition(v, TimeWarp.fixedDeltaTime);//v.CoM; MissileBase missile = null; Vector3 vectorToTarget = v.transform.position - vesselTransform.position; float distanceToTarget = vectorToTarget.magnitude; - float planarDistanceToTarget = Vector3.ProjectOnPlane(vectorToTarget, upDirection).magnitude; + float planarDistanceToTarget = vectorToTarget.ProjectOnPlanePreNormalized(upDirection).magnitude; float angleToTarget = Vector3.Angle(target - vesselTransform.position, vesselTransform.up); float strafingDistance = -1f; float relativeVelocity = (float)(vessel.srf_velocity - v.srf_velocity).magnitude; + if (weaponManager) { + if (!weaponManager.staleTarget) staleTargetVelocity = Vector3.zero; //if actively tracking target, reset last known velocity vector missile = weaponManager.CurrentMissile; if (missile != null) { @@ -1496,8 +2068,8 @@ void FlyToTargetVessel(FlightCtrlState s, Vessel v) if (missile.TargetingMode == MissileBase.TargetingModes.Heat && !weaponManager.heatTarget.exists) { - debugString.AppendLine($"Attempting heat lock"); - target += v.srf_velocity.normalized * 10; + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) debugString.AppendLine($"Attempting heat lock"); + target += v.srf_velocity.normalized * 10; //TODO this should be based on heater boresight? } else { @@ -1539,10 +2111,10 @@ void FlyToTargetVessel(FlightCtrlState s, Vessel v) float targetAngVel = Vector3.Angle(v.transform.position - vessel.transform.position, v.transform.position + (vessel.Velocity()) - vessel.transform.position); float magnifier = Mathf.Clamp(targetAngVel, 1f, 2f); magnifier += ((magnifier - 1f) * Mathf.Sin(Time.time * 0.75f)); - debugString.AppendLine($"targetAngVel: {targetAngVel:F4}, magnifier: {magnifier:F2}"); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) debugString.AppendLine($"targetAngVel: {targetAngVel:F4}, magnifier: {magnifier:F2}"); target -= magnifier * leadOffset; // The effect of this is to exagerate the lead if the angular velocity is > 1 angleToTarget = Vector3.Angle(vesselTransform.up, target - vesselTransform.position); - if (distanceToTarget < weaponManager.gunRange && angleToTarget < 20) + if (distanceToTarget < weaponManager.gunRange && angleToTarget < 20) // FIXME This ought to be changed to a dynamic angle like the firing angle. { steerMode = SteerModes.Aiming; //steer to aim } @@ -1581,7 +2153,8 @@ void FlyToTargetVessel(FlightCtrlState s, Vessel v) steerMode = SteerModes.Aiming; } } - else if (distanceToTarget > weaponManager.gunRange * 1.5f || Vector3.Dot(target - vesselTransform.position, vesselTransform.up) < 0) // Target is airborne a long way away or behind us. + //else if (distanceToTarget > weaponManager.gunRange * 1.5f || Vector3.Dot(target - vesselTransform.position, vesselTransform.up) < 0) // Target is airborne a long way away or behind us. + else if (Vector3.Dot(target - vesselTransform.position, vesselTransform.up) < 0) //If a gun is selected, craft is probably already within gunrange, or a couple of seconds of being in gunrange { target = v.CoM; // Don't bother with the off-by-one physics frame correction as this doesn't need to be so accurate here. } @@ -1590,20 +2163,26 @@ void FlyToTargetVessel(FlightCtrlState s, Vessel v) else if (planarDistanceToTarget > weaponManager.gunRange * 1.25f && (vessel.altitude < v.altitude || (float)vessel.radarAltitude < defaultAltitude)) //climb to target vessel's altitude if lower and still too far for guns { finalMaxSteer = GetSteerLimiterForSpeedAndPower(); - if (v.LandedOrSplashed) vectorToTarget += upDirection * defaultAltitude; // If the target is landed or splashed, aim for the default altitude whiel we're outside our gun's range. + if (v.LandedOrSplashed) vectorToTarget += upDirection * defaultAltitude; // If the target is landed or splashed, aim for the default altitude while we're outside our gun's range. target = vesselTransform.position + GetLimitedClimbDirectionForSpeed(vectorToTarget); } + //change target offset if no selected weapon and at target alt? target += targetVelocity * closing time? else { finalMaxSteer = GetSteerLimiterForSpeedAndPower(); } + if (weaponManager.staleTarget) //lost track of target, but know it's in general area, simulate location estimate precision decay over time + { + if (staleTargetVelocity == Vector3.zero) staleTargetVelocity = v.Velocity(); //if lost target, follow last known velocity vector + target += staleTargetPosition + staleTargetVelocity * weaponManager.detectedTargetTimeout; + } } float targetDot = Vector3.Dot(vesselTransform.up, v.transform.position - vessel.transform.position); //manage speed when close to enemy float finalMaxSpeed = maxSpeed; - if (targetDot > 0f) // Target is ahead. + if (steerMode == SteerModes.Aiming) // Target is ahead and we're trying to aim at them. Outside this angle, we want full thrust to turn faster onto the target. { if (strafingDistance < 0f) // target flying, or beyond range of beginning strafing run for landed/splashed targets. { @@ -1613,7 +2192,7 @@ void FlyToTargetVessel(FlightCtrlState s, Vessel v) { //Mathf.Max(finalMaxSpeed = (distanceToTarget - vesselStandoffDistance) / 8f + (float)v.srfSpeed, 0); //for less aggressive braking finalMaxSpeed = distanceToTarget / vesselStandoffDistance * (float)v.srfSpeed; // Within stand-off distance, back off the thottle a bit. - debugString.AppendLine($"Getting too close to Enemy. Braking!"); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) debugString.AppendLine($"Getting too close to Enemy. Braking!"); } } else @@ -1627,15 +2206,18 @@ void FlyToTargetVessel(FlightCtrlState s, Vessel v) if ((targetDot < 0 && vessel.srfSpeed > finalMaxSpeed) && distanceToTarget < 300 && vessel.srfSpeed < v.srfSpeed * 1.25f && Vector3.Dot(vessel.Velocity(), v.Velocity()) > 0) //distance is less than 800m { - debugString.AppendLine($"Enemy on tail. Braking!"); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) debugString.AppendLine($"Enemy on tail. Braking!"); AdjustThrottle(minSpeed, true); } - if (missile != null - && targetDot > 0 - && distanceToTarget < MissileLaunchParams.GetDynamicLaunchParams(missile, v.Velocity(), v.transform.position).minLaunchRange - && vessel.srfSpeed > idleSpeed) + + if (missile != null) { - RequestExtend("too close for missile", v); // Get far enough away to use the missile. + float boresightFactor = (vessel.LandedOrSplashed || v.LandedOrSplashed || missile.uncagedLock) ? 0.75f : 0.35f; + float minDynamicLaunchRange = MissileLaunchParams.GetDynamicLaunchParams(missile, v.Velocity(), v.transform.position, missile.maxOffBoresight * boresightFactor).minLaunchRange; + if (canExtend && targetDot > 0 && distanceToTarget < minDynamicLaunchRange && vessel.srfSpeed > idleSpeed) + { + RequestExtend($"too close for missile: {minDynamicLaunchRange}m", v, minDynamicLaunchRange, missile: missile); // Get far enough away to use the missile. + } } if (regainEnergy && angleToTarget > 30f) @@ -1645,6 +2227,7 @@ void FlyToTargetVessel(FlightCtrlState s, Vessel v) } else { + debugString.AppendLine($"AngleToTarget ({v.vesselName}): {angleToTarget}° Dot: {Vector3.Dot((target - vesselTransform.position).normalized, vesselTransform.up):F6}"); useVelRollTarget = true; FlyToPosition(s, target); return; @@ -1653,16 +2236,18 @@ void FlyToTargetVessel(FlightCtrlState s, Vessel v) void RegainEnergy(FlightCtrlState s, Vector3 direction, float throttleOverride = -1f) { - debugString.AppendLine($"Regaining energy"); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) debugString.AppendLine($"Regaining energy"); steerMode = SteerModes.Aiming; - Vector3 planarDirection = Vector3.ProjectOnPlane(direction, upDirection); + Vector3 planarDirection = direction.ProjectOnPlanePreNormalized(upDirection); float angle = (Mathf.Clamp((float)vessel.radarAltitude - minAltitude, 0, 1500) / 1500) * 90; angle = Mathf.Clamp(angle, 0, 55) * Mathf.Deg2Rad; Vector3 targetDirection = Vector3.RotateTowards(planarDirection, -upDirection, angle, 0); targetDirection = Vector3.RotateTowards(vessel.Velocity(), targetDirection, 15f * Mathf.Deg2Rad, 0).normalized; + throttleOverride = (FlatSpin == 0) ? throttleOverride : 0f; + if (throttleOverride >= 0) AdjustThrottle(maxSpeed, false, true, false, throttleOverride); else @@ -1676,10 +2261,24 @@ float GetSteerLimiterForSpeedAndPower() float possibleAccel = speedController.GetPossibleAccel(); float speed = (float)vessel.srfSpeed; - debugString.AppendLine($"possibleAccel: {possibleAccel}"); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) debugString.AppendLine($"possibleAccel: {possibleAccel}"); float limiter = ((speed - minSpeed) / 2 / minSpeed) + possibleAccel / 15f; // FIXME The calculation for possibleAccel needs further investigation. - debugString.AppendLine($"unclamped limiter: { limiter}"); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) debugString.AppendLine($"unclamped limiter: {limiter}"); + + return Mathf.Clamp01(limiter); + } + + float GetUserDefinedSteerLimit() + { + float limiter = 1; + if (maxSteer > maxSteerAtMaxSpeed) + limiter *= Mathf.Clamp((maxSteerAtMaxSpeed - maxSteer) / (cornerSpeed - lowSpeedSwitch + 0.001f) * ((float)vessel.srfSpeed - lowSpeedSwitch) + maxSteer, maxSteerAtMaxSpeed, maxSteer); // Linearly varies between two limits, clamped at limit values + else + limiter *= Mathf.Clamp((maxSteerAtMaxSpeed - maxSteer) / (cornerSpeed - lowSpeedSwitch + 0.001f) * ((float)vessel.srfSpeed - lowSpeedSwitch) + maxSteer, maxSteer, maxSteerAtMaxSpeed); // Linearly varies between two limits, clamped at limit values + if (altitudeSteerLimiterFactor != 0 && vessel.altitude > altitudeSteerLimiterAltitude) + limiter *= Mathf.Pow((float)vessel.altitude / altitudeSteerLimiterAltitude, altitudeSteerLimiterFactor); // Scale based on altitude relative to the user-defined limit. + limiter *= 1.225f / (float)vessel.atmDensity; // Scale based on atmospheric density relative to sea level Kerbin (since dynamic pressure depends on density) return Mathf.Clamp01(limiter); } @@ -1697,6 +2296,7 @@ void FlyToPosition(FlightCtrlState s, Vector3 targetPosition, bool overrideThrot targetPosition = vessel.transform.position + vessel.Velocity(); } + targetPosition = LongRangeAltitudeCorrection(targetPosition); //have this only trigger in atmo? targetPosition = FlightPosition(targetPosition, minAltitude); targetPosition = vesselTransform.position + ((targetPosition - vesselTransform.position).normalized * 100); } @@ -1712,19 +2312,14 @@ void FlyToPosition(FlightCtrlState s, Vector3 targetPosition, bool overrideThrot Vector3 localAngVel = vessel.angularVelocity; //test Vector3 currTargetDir = (targetPosition - vesselTransform.position).normalized; - // if (steerMode == SteerModes.NormalFlight) // This block was doing nothing... what was it originally for? - // { - // float gRotVel = ((10f * maxAllowedGForce) / ((float)vessel.srfSpeed)); - // //currTargetDir = Vector3.RotateTowards(prevTargetDir, currTargetDir, gRotVel*Mathf.Deg2Rad, 0); - // } if (IsExtending || IsEvading) // If we're extending or evading, add a deviation to the fly-to direction to make us harder to hit. { var squigglySquidTime = 90f * (float)vessel.missionTime + 8f * Mathf.Sin((float)vessel.missionTime * 6.28f) + 16f * Mathf.Sin((float)vessel.missionTime * 3.14f); // Vary the rate around 90°/s to be more unpredictable. - var squigglySquidDirection = Quaternion.AngleAxis(evasionNonlinearityDirection * squigglySquidTime, currTargetDir) * Vector3.ProjectOnPlane(upDirection, currTargetDir).normalized; + var squigglySquidDirection = Quaternion.AngleAxis(evasionNonlinearityDirection * squigglySquidTime, currTargetDir) * upDirection.ProjectOnPlanePreNormalized(currTargetDir).normalized; #if DEBUG - DEBUG_vector = squigglySquidDirection; + debugSquigglySquidDirection = squigglySquidDirection; #endif - debugString.AppendLine($"Squiggly Squid: {Vector3.Angle(currTargetDir, Vector3.RotateTowards(currTargetDir, squigglySquidDirection, evasionNonlinearity * Mathf.Deg2Rad, 0f))}° at {((squigglySquidTime) % 360f).ToString("G3")}°"); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) debugString.AppendLine($"Squiggly Squid: {Vector3.Angle(currTargetDir, Vector3.RotateTowards(currTargetDir, squigglySquidDirection, evasionNonlinearity * Mathf.Deg2Rad, 0f))}° at {((squigglySquidTime) % 360f).ToString("G3")}°"); currTargetDir = Vector3.RotateTowards(currTargetDir, squigglySquidDirection, evasionNonlinearity * Mathf.Deg2Rad, 0f); } Vector3 targetAngVel = Vector3.Cross(prevTargetDir, currTargetDir) / Time.fixedDeltaTime; @@ -1734,18 +2329,37 @@ void FlyToPosition(FlightCtrlState s, Vector3 targetPosition, bool overrideThrot flyingToPosition = targetPosition; + if (avoidingTerrain) + { + steerMode = SteerModes.Aiming; // Set aiming steer mode so yaw has a bigger effect. + } + //test poststall float AoA = Vector3.Angle(vessel.ReferenceTransform.up, vessel.Velocity()); - if (AoA > maxAllowedAoA) + if (AoA > postStallAoA) + { + isPSM = true; + steerMode = SteerModes.Aiming; + } + else + { + isPSM = false; + } + + float angleToTarget = Vector3.Angle(targetPosition - vesselTransform.position, vesselTransform.up); + if (autoTune && (Mathf.Abs(angleToTarget) < 20f)) { steerMode = SteerModes.Aiming; } - //slow down for tighter turns - float velAngleToTarget = Mathf.Clamp(Vector3.Angle(targetPosition - vesselTransform.position, vessel.Velocity()), 0, 90); + //slow down for tighter turns, unless we're already at high AoA, in which case we want more thrust float speedReductionFactor = 1.25f; - float finalSpeed = Mathf.Min(speedController.targetSpeed, Mathf.Clamp(maxSpeed - (speedReductionFactor * velAngleToTarget), idleSpeed, maxSpeed)); - debugString.AppendLine($"Final Target Speed: {finalSpeed}"); + float finalSpeed; + // float velAngleToTarget = Mathf.Clamp(Vector3.Angle(targetPosition - vesselTransform.position, vessel.Velocity()), 0, 90); + // if (vessel.atmDensity > 0.05f) finalSpeed = Mathf.Min(speedController.targetSpeed, Mathf.Clamp(maxSpeed - (speedReductionFactor * velAngleToTarget), idleSpeed, maxSpeed)); + if (vessel.atmDensity > 0.05f) finalSpeed = Mathf.Min(speedController.targetSpeed, Mathf.Clamp(maxSpeed - speedReductionFactor * (angleToTarget - AoA), idleSpeed, maxSpeed)); + else finalSpeed = Mathf.Min(speedController.targetSpeed, maxSpeed); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) debugString.AppendLine($"Final Target Speed: {finalSpeed}"); if (!overrideThrottle) { @@ -1768,7 +2382,7 @@ void FlyToPosition(FlightCtrlState s, Vector3 targetPosition, bool overrideThrot targetDirection = velocityTransform.InverseTransformDirection(targetPosition - velocityTransform.position).normalized; targetDirection = Vector3.RotateTowards(Vector3.up, targetDirection, 45 * Mathf.Deg2Rad, 0); - if (useWaypointYawAuthority && IsFlyingWaypoints) + if (useWaypointYawAuthority && IsRunningWaypoints) { var refYawDir = Vector3.RotateTowards(Vector3.up, vesselTransform.InverseTransformDirection(targetPosition - vesselTransform.position), 25 * Mathf.Deg2Rad, 0).normalized; var velYawDir = Vector3.RotateTowards(Vector3.up, vesselTransform.InverseTransformDirection(vessel.Velocity()), 45 * Mathf.Deg2Rad, 0).normalized; @@ -1789,25 +2403,22 @@ void FlyToPosition(FlightCtrlState s, Vector3 targetPosition, bool overrideThrot debugPos = vessel.transform.position + (targetPosition - vesselTransform.position) * 5000; //// Adjust targetDirection based on ATTITUDE limits - //var horizonUp = Vector3.ProjectOnPlane(vesselTransform.up, upDirection).normalized; + // var horizonUp = vesselTransform.up.ProjectOnPlanePreNormalized(upDirection).normalized; //var horizonRight = -Vector3.Cross(horizonUp, upDirection); //float attitude = Vector3.SignedAngle(horizonUp, vesselTransform.up, horizonRight); //if ((Mathf.Abs(attitude) > maxAttitude) && (maxAttitude != 90f)) //{ // var projectPlane = Vector3.RotateTowards(upDirection, horizonUp, attitude * Mathf.PI / 180f, 0f); - // targetDirection = Vector3.ProjectOnPlane(targetDirection, projectPlane); + // targetDirection = targetDirection.ProjectOnPlanePreNormalized(projectPlane); //} //debugString.AppendLine($"Attitude: " + attitude); - pitchError = VectorUtils.SignedAngle(Vector3.up, Vector3.ProjectOnPlane(targetDirection, Vector3.right), Vector3.back); - yawError = VectorUtils.SignedAngle(Vector3.up, Vector3.ProjectOnPlane(targetDirectionYaw, Vector3.forward), Vector3.right); + pitchError = VectorUtils.SignedAngle(Vector3.up, targetDirection.ProjectOnPlanePreNormalized(Vector3.right), Vector3.back); + yawError = VectorUtils.SignedAngle(Vector3.up, targetDirectionYaw.ProjectOnPlanePreNormalized(Vector3.forward), Vector3.right); // User-set steer limits - if (maxSteer > maxSteerAtMaxSpeed) - finalMaxSteer *= Mathf.Clamp((maxSteerAtMaxSpeed - maxSteer) / (cornerSpeed - lowSpeedSwitch + 0.001f) * ((float)vessel.srfSpeed - lowSpeedSwitch) + maxSteer, maxSteerAtMaxSpeed, maxSteer); // Linearly varies between two limits, clamped at limit values - else - finalMaxSteer *= Mathf.Clamp((maxSteerAtMaxSpeed - maxSteer) / (cornerSpeed - lowSpeedSwitch + 0.001f) * ((float)vessel.srfSpeed - lowSpeedSwitch) + maxSteer, maxSteer, maxSteerAtMaxSpeed); // Linearly varies between two limits, clamped at limit values - finalMaxSteer *= 1.225f / (float)vessel.atmDensity; // Scale based on atmospheric density relative to sea level Kerbin (since dynamic pressure depends on density) + float userLimit = GetUserDefinedSteerLimit(); + finalMaxSteer *= userLimit; finalMaxSteer = Mathf.Clamp(finalMaxSteer, 0.1f, 1f); // added just in case to ensure some input is retained no matter what happens //roll @@ -1832,13 +2443,18 @@ void FlyToPosition(FlightCtrlState s, Vector3 targetPosition, bool overrideThrot } bool requiresLowAltitudeRollTargetCorrection = false; - if (belowMinAltitude) + if (avoidingTerrain) { - if (avoidingTerrain) - rollTarget = terrainAlertNormal * 100; - else - rollTarget = vessel.upAxis * 100; + rollTarget = terrainAlertNormal * 100; + var terrainAvoidanceRollCosAngle = Vector3.Dot(-vesselTransform.forward, terrainAlertNormal.ProjectOnPlanePreNormalized(vesselTransform.up).normalized); + if (terrainAvoidanceRollCosAngle < terrainAvoidanceCriticalCosAngle) + { + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) debugString.AppendLine($"inverting rollTarget: {rollTarget}, cosAngle: {terrainAvoidanceRollCosAngle} vs {terrainAvoidanceCriticalCosAngle}, isPSM: {isPSM}"); + rollTarget = -rollTarget; // Avoid terrain fully inverted if the plane is mostly inverted (>30°) to begin with. + } } + else if (belowMinAltitude && !gainAltInhibited) + rollTarget = Vector3.Lerp(BodyUtils.GetSurfaceNormal(vesselTransform.position), upDirection, (float)vessel.radarAltitude / minAltitude) * 100; // Adjust the roll target smoothly from the surface normal to upwards to avoid clipping wings into terrain on take-off. else if (!avoidingTerrain && vessel.verticalSpeed < 0 && Vector3.Dot(rollTarget, upDirection) < 0 && Vector3.Dot(rollTarget, vessel.Velocity()) < 0) // If we're not avoiding terrain, heading downwards and the roll target is behind us and downwards, check that a circle arc of radius "turn radius" (scaled by twiddle factor minimum) tilted at angle of rollTarget has enough room to avoid hitting the ground. { // The following calculates the altitude required to turn in the direction of the rollTarget based on the current velocity and turn radius. @@ -1847,53 +2463,54 @@ void FlyToPosition(FlightCtrlState s, Vector3 targetPosition, bool overrideThrot var m = Vector3.Cross(n, upDirection).normalized; // cos(theta) = dot(m,v). if (m.magnitude < 0.1f) m = upDirection; // In case n and upDirection are colinear. var a = Vector3.Dot(n, upDirection); // sin(phi) = dot(n,up) - var b = Mathf.Sqrt(1f - a * a); // cos(phi) = sqrt(1-sin(phi)^2) - var r = turnRadius * turnRadiusTwiddleFactorMin; // Radius of turning circle. + var b = BDAMath.Sqrt(1f - a * a); // cos(phi) = sqrt(1-sin(phi)^2) + var r = turnRadiusTwiddleFactorMax * turnRadius + 0.5f * (float)vessel.srfSpeed * controlSurfaceDeploymentTime; // Worst-case radius of turning circle. (We use the max and 1/2 ctrl srf deploy time to avoid triggering terrain avoidance since we're inverted even though the plane ought to be able to manage the turn with the min.) + var h = r * (1 + Vector3.Dot(m, vessel.srf_vel_direction)) * b; // Required altitude: h = r * (1+cos(theta)) * cos(phi). if (vessel.radarAltitude < h) // Too low for this manoeuvre. { - // Debug.Log($"DEBUG {vessel.vesselName} too low for rollTarget; v: {vessel.srf_vel_direction.ToString("G3")}, t: {rollTarget.ToString("G3")}, r: {r}, h: {h}, alt: {vessel.radarAltitude}, up: {upDirection.ToString("G3")}, phi: {Mathf.Rad2Deg * Mathf.Acos(b)}"); requiresLowAltitudeRollTargetCorrection = true; // For simplicity, we'll apply the correction after the projections have occurred. } + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) debugString.AppendLine($"Low-alt loop: {requiresLowAltitudeRollTargetCorrection:G4}: {vessel.radarAltitude:G4} < {h:G4}, r: {r}"); } - if (useWaypointRollTarget && IsFlyingWaypoints) + if (useWaypointRollTarget && IsRunningWaypoints) { var angle = waypointRollTargetStrength * Vector3.Angle(waypointRollTarget, rollTarget); - rollTarget = Vector3.ProjectOnPlane(Vector3.RotateTowards(rollTarget, waypointRollTarget, angle * Mathf.Deg2Rad, 0f), vessel.Velocity()); + rollTarget = Vector3.RotateTowards(rollTarget, waypointRollTarget, angle * Mathf.Deg2Rad, 0f).ProjectOnPlane(vessel.Velocity()); } else if (useVelRollTarget && !belowMinAltitude) { - rollTarget = Vector3.ProjectOnPlane(rollTarget, vessel.Velocity()); - currentRoll = Vector3.ProjectOnPlane(currentRoll, vessel.Velocity()); + rollTarget = rollTarget.ProjectOnPlane(vessel.Velocity()); + currentRoll = currentRoll.ProjectOnPlane(vessel.Velocity()); } else { - rollTarget = Vector3.ProjectOnPlane(rollTarget, vesselTransform.up); + rollTarget = rollTarget.ProjectOnPlanePreNormalized(vesselTransform.up); } //ramming if (ramming) - rollTarget = Vector3.ProjectOnPlane(targetPosition - vesselTransform.position + rollUp * Mathf.Clamp((targetPosition - vesselTransform.position).magnitude / 500f, 0f, 1f) * upDirection, vesselTransform.up); + rollTarget = (targetPosition - vesselTransform.position + rollUp * Mathf.Clamp((targetPosition - vesselTransform.position).magnitude / 500f, 0f, 1f) * upDirection).ProjectOnPlanePreNormalized(vesselTransform.up); if (requiresLowAltitudeRollTargetCorrection) // Low altitude downwards loop prevention to avoid triggering terrain avoidance. { // Set the roll target to be horizontal. - rollTarget = Vector3.ProjectOnPlane(rollTarget, upDirection).normalized * 100; + rollTarget = rollTarget.ProjectOnPlanePreNormalized(upDirection).normalized * 100; } // Limit Bank Angle, this should probably be re-worked using quaternions or something like that, SignedAngle doesn't work well for angles > 90 - Vector3 horizonNormal = Vector3.ProjectOnPlane(vessel.transform.position - vessel.mainBody.transform.position, vesselTransform.up); + Vector3 horizonNormal = (vessel.transform.position - vessel.mainBody.transform.position).ProjectOnPlanePreNormalized(vesselTransform.up); float bankAngle = Vector3.SignedAngle(horizonNormal, rollTarget, vesselTransform.up); if ((Mathf.Abs(bankAngle) > maxBank) && (maxBank != 180)) rollTarget = Vector3.RotateTowards(horizonNormal, rollTarget, maxBank / 180 * Mathf.PI, 0.0f); bankAngle = Vector3.SignedAngle(horizonNormal, rollTarget, vesselTransform.up); - float rollError = Utils.SignedAngle(currentRoll, rollTarget, vesselTransform.right); - if (steerMode == SteerModes.NormalFlight && !avoidingTerrain && evasiveTimer == 0 && currentlyAvoidedVessel == null) // Don't apply this fix while avoiding terrain, makes it difficult for craft to exit dives; or evading or avoiding other vessels as we need a quick reaction - { - //premature dive fix - pitchError = pitchError * Mathf.Clamp01((21 - Mathf.Exp(Mathf.Abs(rollError) / 30)) / 20); - } + float rollError = BDAMath.SignedAngle(currentRoll, rollTarget, vesselTransform.right); + // if (steerMode == SteerModes.NormalFlight && !avoidingTerrain && evasiveTimer == 0 && currentlyAvoidedVessel == null) // Don't apply this fix while avoiding terrain, makes it difficult for craft to exit dives; or evading or avoiding other vessels as we need a quick reaction + // { + // //premature dive fix + // pitchError = pitchError * Mathf.Clamp01((21 - Mathf.Exp(Mathf.Abs(rollError) / 30)) / 20); + // } #region PID calculations // FIXME Why are there various constants in here that mess with the scaling of the PID in the various axes? Ratios between the axes are 1:0.33:0.1 @@ -1901,12 +2518,12 @@ void FlyToPosition(FlightCtrlState s, Vector3 targetPosition, bool overrideThrot float yawProportional = 0.005f * steerMult * yawError; float rollProportional = 0.0015f * steerMult * rollError; - float pitchDamping = SteerDamping(Mathf.Abs(Vector3.Angle(targetPosition - vesselTransform.position, vesselTransform.up)), Vector3.Angle(targetPosition - vesselTransform.position, vesselTransform.up), 1) * -localAngVel.x; - float yawDamping = 0.33f * SteerDamping(Mathf.Abs(yawError * (steerMode == SteerModes.Aiming ? (180f / 25f) : 4f)), Vector3.Angle(targetPosition - vesselTransform.position, vesselTransform.up), 2) * -localAngVel.z; - float rollDamping = 0.1f * SteerDamping(Mathf.Abs(rollError), Vector3.Angle(targetPosition - vesselTransform.position, vesselTransform.up), 3) * -localAngVel.y; + float pitchDamping = SteerDamping(Mathf.Abs(angleToTarget), angleToTarget, 1) * -localAngVel.x; + float yawDamping = 0.33f * SteerDamping(Mathf.Abs(yawError * (steerMode == SteerModes.Aiming ? (180f / 25f) : 4f)), angleToTarget, 2) * -localAngVel.z; + float rollDamping = 0.1f * SteerDamping(Mathf.Abs(rollError), angleToTarget, 3) * -localAngVel.y; // For the integral, we track the vector of the pitch and yaw in the 2D plane of the vessel's forward pointing vector so that the pitch and yaw components translate between the axes when the vessel rolls. - directionIntegral = Vector3.ProjectOnPlane(directionIntegral + (pitchError * -vesselTransform.forward + yawError * vesselTransform.right) * Time.deltaTime, vesselTransform.up); + directionIntegral = (directionIntegral + (pitchError * -vesselTransform.forward + yawError * vesselTransform.right) * Time.deltaTime).ProjectOnPlanePreNormalized(vesselTransform.up); if (directionIntegral.sqrMagnitude > 1f) directionIntegral = directionIntegral.normalized; pitchIntegral = steerKiAdjust * Vector3.Dot(directionIntegral, -vesselTransform.forward); yawIntegral = 0.33f * steerKiAdjust * Vector3.Dot(directionIntegral, vesselTransform.right); @@ -1923,11 +2540,15 @@ void FlyToPosition(FlightCtrlState s, Vector3 targetPosition, bool overrideThrot steerYaw *= dynamicAdjustment; steerRoll *= dynamicAdjustment; - s.pitch = Mathf.Clamp(steerPitch, Mathf.Min(-finalMaxSteer, -0.2f), finalMaxSteer); // finalMaxSteer for pitch and yaw, but not roll. - s.yaw = Mathf.Clamp(steerYaw, -finalMaxSteer, finalMaxSteer); - s.roll = Mathf.Clamp(steerRoll, -maxSteer, maxSteer); + SetFlightControlState(s, + Mathf.Clamp(steerPitch, Mathf.Min(-finalMaxSteer, -0.2f), finalMaxSteer), // pitch + Mathf.Clamp(steerYaw, -finalMaxSteer, finalMaxSteer), // yaw + Mathf.Clamp(steerRoll, -userLimit, userLimit)); // roll + + if (autoTune) + { pidAutoTuning.Update(pitchError, rollError, yawError); } - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) { debugString.AppendLine(String.Format("steerMode: {0}, rollError: {1,7:F4}, pitchError: {2,7:F4}, yawError: {3,7:F4}", steerMode, rollError, pitchError, yawError)); debugString.AppendLine($"finalMaxSteer: {finalMaxSteer:G3}, dynAdj: {dynamicAdjustment:G3}"); @@ -1952,6 +2573,16 @@ bool CheckExtend(ExtendChecks checkType = ExtendChecks.All) StopExtending("target override"); return false; } + if (extendAbortTimer < 0) // In cooldown, extending disabled. + { + StopExtending("in cooldown"); + return false; + } + if (ramming) // Disable extending if in ramming mode. + { + StopExtending("ramming speed"); + return false; + } if (!extending) { extendParametersSet = false; // Reset this flag for new extends. @@ -1960,14 +2591,25 @@ bool CheckExtend(ExtendChecks checkType = ExtendChecks.All) if (requestedExtend) { requestedExtend = false; - extending = true; - lastTargetPosition = requestedExtendTpos; + if (CheckRequestedExtendDistance()) + { + extending = true; + lastTargetPosition = requestedExtendTpos; + } } if (checkType == ExtendChecks.RequestsOnly) return extending; if (extending && extendParametersSet) { if (extendTarget != null) // Update the last known target position. - { lastTargetPosition = extendTarget.CoM; } + { + lastTargetPosition = extendTarget.CoM; + if (extendForMissile != null) // If extending to fire a missile, update the extend distance for the dynamic launch range. + { + float boresightFactor = (vessel.LandedOrSplashed || extendTarget.LandedOrSplashed || extendForMissile.uncagedLock) ? 0.75f : 0.35f; + var minDynamicLaunchRange = MissileLaunchParams.GetDynamicLaunchParams(extendForMissile, extendTarget.Velocity(), extendTarget.transform.position, extendForMissile.maxOffBoresight * boresightFactor).minLaunchRange; + extendDistance = Mathf.Max(extendDistanceAirToAir, minDynamicLaunchRange); + } + } return true; // Already extending. } if (!wasEvading) evasionNonlinearityDirection = Mathf.Sign(UnityEngine.Random.Range(-1f, 1f)); // This applies to extending too. @@ -1975,10 +2617,10 @@ bool CheckExtend(ExtendChecks checkType = ExtendChecks.All) // Dropping a bomb. if (extending && weaponManager.CurrentMissile && weaponManager.CurrentMissile.GetWeaponClass() == WeaponClasses.Bomb) // Run away from the bomb! { - extendDistance = 4500; + extendDistance = extendRequestMinDistance; //4500; //what, are we running from nukes? blast radius * 1.5 should be sufficient desiredMinAltitude = defaultAltitude; extendParametersSet = true; - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log($"[BDArmory.BDModulePilotAI]: {vessel.vesselName} is extending due to dropping a bomb!"); + if (BDArmorySettings.DEBUG_AI) Debug.Log($"[BDArmory.BDModulePilotAI]: {Time.time:F3} {vessel.vesselName} is extending due to dropping a bomb!"); return true; } @@ -2013,7 +2655,7 @@ bool CheckExtend(ExtendChecks checkType = ExtendChecks.All) lastTargetPosition = targetVessel.transform.position; extendTarget = targetVessel; extendParametersSet = true; - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log($"[BDArmory.BDModulePilotAI]: {vessel.vesselName} is extending due to a ground target."); + if (BDArmorySettings.DEBUG_AI) Debug.Log($"[BDArmory.BDModulePilotAI]: {Time.time:F3} {vessel.vesselName} is extending due to a ground target."); return true; } } @@ -2022,11 +2664,11 @@ bool CheckExtend(ExtendChecks checkType = ExtendChecks.All) // Air target (from requests, where extendParameters haven't been set yet). if (extending && extendTarget != null && !extendTarget.LandedOrSplashed) // We have a flying target, only extend a short distance and don't climb. { - extendDistance = extendDistanceAirToAir; + extendDistance = Mathf.Max(extendDistanceAirToAir, extendRequestMinDistance); extendHorizontally = false; - desiredMinAltitude = (float)vessel.radarAltitude * 0.95f; // Extend mostly horizontally + desiredMinAltitude = Mathf.Max((float)vessel.radarAltitude + _extendAngleAirToAir * extendDistance, minAltitude); extendParametersSet = true; - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log($"[BDArmory.BDModulePilotAI]: {vessel.vesselName} is extending due to an air target ({extendingReason})."); + if (BDArmorySettings.DEBUG_AI) Debug.Log($"[BDArmory.BDModulePilotAI]: {Time.time:F3} {vessel.vesselName} is extending due to an air target ({extendingReason})."); return true; } @@ -2034,14 +2676,52 @@ bool CheckExtend(ExtendChecks checkType = ExtendChecks.All) return false; } + /// + /// Check whether the extend distance condition would not already be satisfied. + /// + /// True if the requested extend distance is not already satisfied. + bool CheckRequestedExtendDistance() + { + if (extendTarget == null) return true; // Dropping a bomb or similar. + float localExtendDistance = 1f; + Vector3 extendVector = default; + if (!extendTarget.LandedOrSplashed) // Airborne target. + { + localExtendDistance = Mathf.Max(extendDistanceAirToAir, extendRequestMinDistance); + extendVector = vessel.transform.position - requestedExtendTpos; + } + else return true; // Ignore non-airborne targets for now. Currently, requests are only made for air-to-air targets and for dropping bombs. + return extendVector.sqrMagnitude < localExtendDistance * localExtendDistance; // Extend from position is further than the extend distance. + } + void FlyExtend(FlightCtrlState s, Vector3 tPosition) { - var extendVector = extendHorizontally ? Vector3.ProjectOnPlane(vessel.transform.position - tPosition, upDirection) : vessel.transform.position - tPosition; - if (extendVector.sqrMagnitude < extendDistance * extendDistance) // Extend from position is closer (horizontally) than the extend distance. + var extendVector = extendHorizontally ? (vessel.transform.position - tPosition).ProjectOnPlanePreNormalized(upDirection) : vessel.transform.position - tPosition; + var extendDistanceSqr = extendVector.sqrMagnitude; + if (extendDistanceSqr < extendDistance * extendDistance) // Extend from position is closer (horizontally) than the extend distance. { + if (extendDistanceSqr > lastExtendDistanceSqr) // Gaining distance. + { + if (extendAbortTimer > 0) // Reduce the timer to 0. + { + extendAbortTimer -= 0.5f * TimeWarp.fixedDeltaTime; // Reduce at half the rate of increase, so oscillating pairs of craft eventually time out and abort. + if (extendAbortTimer < 0) extendAbortTimer = 0; + } + } + else // Not gaining distance. + { + extendAbortTimer += TimeWarp.fixedDeltaTime; + if (extendAbortTimer > extendAbortTime) // Abort if not gaining distance. + { + StopExtending($"extend abort time ({extendAbortTime}s) reached at distance {extendVector.magnitude}m of {extendDistance}m", true); + return; + } + } + lastExtendDistanceSqr = extendDistanceSqr; + Vector3 targetDirection = extendVector.normalized * extendDistance; Vector3 target = vessel.transform.position + targetDirection; // Target extend position horizontally. - target = GetTerrainSurfacePosition(target) + (vessel.upAxis * Mathf.Min(defaultAltitude, MissileGuidance.GetRaycastRadarAltitude(vesselTransform.position))); // Adjust for terrain changes at target extend position. + target += upDirection * (Mathf.Min(defaultAltitude, BodyUtils.GetRadarAltitudeAtPos(vesselTransform.position)) - BodyUtils.GetRadarAltitudeAtPos(target)); // Adjust for terrain changes at target extend position. target = FlightPosition(target, desiredMinAltitude); // Further adjustments for speed, situation, etc. and desired minimum altitude after extending. if (regainEnergy) { @@ -2050,12 +2730,13 @@ void FlyExtend(FlightCtrlState s, Vector3 tPosition) } else { + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) debugString.AppendLine($"Extending: {extendVector.magnitude:F0}m of {extendDistance:F0}m{(extendAbortTimer > 0 ? $" ({extendAbortTimer:F1}s of {extendAbortTime:F1}s)" : "")}"); FlyToPosition(s, target); } } else // We're far enough away, stop extending. { - StopExtending($"gone far enough (" + extendVector.magnitude + " of " + extendDistance + ")"); + StopExtending($"gone far enough ({extendVector.magnitude} of {extendDistance})"); } } @@ -2068,28 +2749,38 @@ void FlyOrbit(FlightCtrlState s, Vector3d centerGPS, float radius, float speed, } finalMaxSteer = GetSteerLimiterForSpeedAndPower(); - debugString.AppendLine($"Flying orbit"); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) debugString.AppendLine($"Flying orbit"); Vector3 flightCenter = GetTerrainSurfacePosition(VectorUtils.GetWorldSurfacePostion(centerGPS, vessel.mainBody)) + (defaultAltitude * upDirection); - - Vector3 myVectorFromCenter = Vector3.ProjectOnPlane(vessel.transform.position - flightCenter, upDirection); + Vector3 myVectorFromCenter = (vessel.transform.position - flightCenter).ProjectOnPlanePreNormalized(upDirection); Vector3 myVectorOnOrbit = myVectorFromCenter.normalized * radius; - - Vector3 targetVectorFromCenter = Quaternion.AngleAxis(clockwise ? 15f : -15f, upDirection) * myVectorOnOrbit; - + Vector3 targetVectorFromCenter = Quaternion.AngleAxis(clockwise ? 15f : -15f, upDirection) * myVectorOnOrbit; // 15° ahead in the orbit. Distance = π*radius/12 Vector3 verticalVelVector = Vector3.Project(vessel.Velocity(), upDirection); //for vv damping - Vector3 targetPosition = flightCenter + targetVectorFromCenter - (verticalVelVector * 0.25f); - + if (vessel.radarAltitude < 1000) + { + var terrainAdjustment = (BodyUtils.GetTerrainAltitudeAtPos(targetPosition) - BodyUtils.GetTerrainAltitudeAtPos(flightCenter)); // Terrain adjustment to avoid throwing planes at terrain when at low altitude. + targetPosition += (1f - (float)vessel.radarAltitude / 1000f) * (float)terrainAdjustment * upDirection; // Fade in adjustment from 1km altitude. + } + if (vessel.radarAltitude < 500) // Terrain slope adjustment when at <500m. + { + Ray ray = new Ray(vesselTransform.position, (targetPosition - vesselTransform.position).normalized); + var distance = Mathf.PI * radius / 12f; + if (Physics.Raycast(ray, out RaycastHit hit, distance, (int)LayerMasks.Scenery)) + { + var slope = ray.direction.ProjectOnPlane(Vector3.Cross(hit.normal, ray.direction)); + targetPosition = targetPosition * (hit.distance / distance) + (1 - hit.distance / distance) * (vesselTransform.position + slope * distance); + } + } Vector3 vectorToTarget = targetPosition - vesselTransform.position; - //Vector3 planarVel = Vector3.ProjectOnPlane(vessel.Velocity(), upDirection); + // Vector3 planarVel = vessel.Velocity().ProjectOnPlanePreNormalized(upDirection); //vectorToTarget = Vector3.RotateTowards(planarVel, vectorToTarget, 25f * Mathf.Deg2Rad, 0); vectorToTarget = GetLimitedClimbDirectionForSpeed(vectorToTarget); targetPosition = vesselTransform.position + vectorToTarget; if (command != PilotCommands.Free && (vessel.transform.position - flightCenter).sqrMagnitude < radius * radius * 1.5f) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BDModulePilotAI]: AI Pilot reached command destination."); - ReleaseCommand(); + if (BDArmorySettings.DEBUG_AI) Debug.Log("[BDArmory.BDModulePilotAI]: AI Pilot reached command destination."); + ReleaseCommand(false, false); } useVelRollTarget = true; @@ -2099,48 +2790,44 @@ void FlyOrbit(FlightCtrlState s, Vector3d centerGPS, float radius, float speed, } #region Waypoints - public void ClearWaypoints() - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BDModulePilotAI]: Cleared waypoints"); - this.waypoints = null; - this.activeWaypointIndex = -1; - } - public void SetWaypoints(List waypoints) - { - if (waypoints == null || waypoints.Count == 0) - { - this.activeWaypointIndex = -1; - this.waypoints = null; - return; - } - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log(string.Format("[BDArmory.BDModulePilotAI]: Set {0} waypoints", waypoints.Count)); - this.waypoints = waypoints; - this.activeWaypointIndex = 0; - var waypoint = waypoints[activeWaypointIndex]; - var terrainAltitude = FlightGlobals.currentMainBody.TerrainAltitude(waypoint.x, waypoint.y); - waypointPosition = FlightGlobals.currentMainBody.GetWorldSurfacePosition(waypoint.x, waypoint.y, waypoint.z + terrainAltitude); - CommandFollowWaypoints(); - } - public bool IsFlyingWaypoints => command == PilotCommands.Waypoints && activeWaypointIndex >= 0 && waypoints != null && waypoints.Count > 0; - - public int GetWaypointIndex() - { - return this.activeWaypointIndex; - } - - private Vector3 waypointPosition = default; - private float waypointRange = 999f; - private Vector3 waypointRollTarget = default; - private float waypointRollTargetStrength = 0; - private bool useWaypointRollTarget = false; - private float waypointYawAuthorityStrength = 0; - private bool useWaypointYawAuthority = false; + Vector3 waypointRollTarget = default; + float waypointRollTargetStrength = 0; + bool useWaypointRollTarget = false; + float waypointYawAuthorityStrength = 0; + bool useWaypointYawAuthority = false; + Ray waypointRay; + RaycastHit waypointRayHit; + bool waypointTerrainAvoidanceActive = false; + Vector3 waypointTerrainSmoothedNormal = default; void FlyWaypoints(FlightCtrlState s) { // Note: UpdateWaypoint is called separately before this in case FlyWaypoints doesn't get called. - SetStatus($"Waypoint {activeWaypointIndex} ({waypointRange:F0}m)"); + if (BDArmorySettings.WAYPOINT_LOOP_INDEX > 1) + { + SetStatus($"Lap {activeWaypointLap}, Waypoint {activeWaypointIndex} ({waypointRange:F0}m)"); + } + else + { + SetStatus($"Waypoint {activeWaypointIndex} ({waypointRange:F0}m)"); + } var waypointDirection = (waypointPosition - vessel.transform.position).normalized; // var waypointDirection = (WaypointSpline() - vessel.transform.position).normalized; + waypointRay = new Ray(vessel.transform.position, waypointDirection); + if (Physics.Raycast(waypointRay, out waypointRayHit, waypointRange, (int)LayerMasks.Scenery)) + { + var angle = 90f + 90f * (1f - waypointTerrainAvoidance) * (waypointRayHit.distance - defaultAltitude) / (waypointRange + 1000f); // Parallel to the terrain at the default altitude (in the direction of the waypoint), adjusted for relative distance to the terrain and the waypoint. 1000 added to waypointRange to provide a stronger effect if the distance to the waypoint is small. + waypointTerrainSmoothedNormal = waypointTerrainAvoidanceActive ? Vector3.Lerp(waypointTerrainSmoothedNormal, waypointRayHit.normal, 0.5f - 0.4862327f * waypointTerrainAvoidanceSmoothingFactor) : waypointRayHit.normal; // Smooth out varying terrain normals at a rate depending on the terrain avoidance strength (half-life of 1s at max avoidance, 0.29s at mid and 0.02s at min avoidance). + waypointDirection = Vector3.RotateTowards(waypointTerrainSmoothedNormal, waypointDirection, angle * Mathf.Deg2Rad, 0f); + waypointTerrainAvoidanceActive = true; + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) debugString.AppendLine($"Waypoint Terrain: {waypointRayHit.distance:F1}m @ {angle:F2}°"); + } + else + { + if (waypointTerrainAvoidanceActive) // Reset stuff + { + waypointTerrainAvoidanceActive = false; + } + } SetWaypointRollAndYaw(); steerMode = SteerModes.NormalFlight; // Make sure we're using the correct steering mode. FlyToPosition(s, vessel.transform.position + waypointDirection * Mathf.Min(500f, waypointRange), false); // Target up to 500m ahead so that max altitude restrictions apply reasonably. @@ -2190,7 +2877,7 @@ private void SetWaypointRollAndYaw() var nextWaypoint = waypoints[activeWaypointIndex + 1]; var terrainAltitude = FlightGlobals.currentMainBody.TerrainAltitude(nextWaypoint.x, nextWaypoint.y); var nextWaypointPosition = FlightGlobals.currentMainBody.GetWorldSurfacePosition(nextWaypoint.x, nextWaypoint.y, nextWaypoint.z + terrainAltitude); - waypointRollTarget = Vector3.ProjectOnPlane(nextWaypointPosition - waypointPosition, vessel.Velocity()).normalized; + waypointRollTarget = (nextWaypointPosition - waypointPosition).ProjectOnPlane(vessel.Velocity()).normalized; waypointRollTargetStrength = Mathf.Min(1f, Vector3.Angle(nextWaypointPosition - waypointPosition, vessel.Velocity()) / maxAllowedAoA) * Mathf.Max(0, 1f - waypointRange / range); // Full strength at maxAllowedAoA and at the waypoint. useWaypointRollTarget = true; } @@ -2203,43 +2890,24 @@ private void SetWaypointRollAndYaw() } } - void UpdateWaypoint() + protected override void UpdateWaypoint() { - if (activeWaypointIndex < 0 || waypoints == null || waypoints.Count == 0) - { - if (command == PilotCommands.Waypoints) ReleaseCommand(); - return; - } + base.UpdateWaypoint(); useWaypointRollTarget = false; // Reset this so that it's only set when actively flying waypoints. useWaypointYawAuthority = false; // Reset this so that it's only set when actively flying waypoints. - var waypoint = waypoints[activeWaypointIndex]; - var terrainAltitude = FlightGlobals.currentMainBody.TerrainAltitude(waypoint.x, waypoint.y); - waypointPosition = FlightGlobals.currentMainBody.GetWorldSurfacePosition(waypoint.x, waypoint.y, waypoint.z + terrainAltitude); - - waypointRange = (float)(vesselTransform.position - waypointPosition).magnitude; - var timeToCPA = AIUtils.ClosestTimeToCPA(vessel.transform.position - waypointPosition, vessel.Velocity(), vessel.acceleration, Time.fixedDeltaTime); - // if (waypointsRange < waypointRadius) Debug.Log($"DEBUG waypoint {activeWaypointIndex}, distance: {waypointsRange:F1} @ {Time.time}, TtCPA: {timeToCPA:F3}"); - if (waypointRange < BDArmorySettings.WAYPOINTS_SCALE && timeToCPA < Time.fixedDeltaTime) // Within waypointRadius and reaching a minimum within the next frame. Looking forwards like this avoids a frame where the fly-to direction is backwards allowing smoother waypoint traversal. - { - // moving away, proceed to next point - var deviation = AIUtils.PredictPosition(vessel.transform.position - waypointPosition, vessel.Velocity(), vessel.acceleration, timeToCPA).magnitude; - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log(string.Format("[BDArmory.BDModulePilotAI]: Reached waypoint {0} with range {1}", activeWaypointIndex, deviation)); - BDACompetitionMode.Instance.Scores.RegisterWaypointReached(vessel.vesselName, activeWaypointIndex, deviation); - ++activeWaypointIndex; - if (activeWaypointIndex >= waypoints.Count) - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BDModulePilotAI]: Waypoints complete"); - waypoints = null; - ReleaseCommand(); - return; - } - else - { - waypoint = waypoints[activeWaypointIndex]; - UpdateWaypoint(); // Call ourselves again for the new waypoint to follow. - return; - } - } + } + + void SetWaypointTerrainAvoidance() + { + UI_FloatRange field = (UI_FloatRange)Fields["waypointTerrainAvoidance"].uiControlEditor; + field.onFieldChanged = OnWaypointTerrainAvoidanceUpdated; + field = (UI_FloatRange)Fields["waypointTerrainAvoidance"].uiControlFlight; + field.onFieldChanged = OnWaypointTerrainAvoidanceUpdated; + OnWaypointTerrainAvoidanceUpdated(null, null); + } + void OnWaypointTerrainAvoidanceUpdated(BaseField field, object obj) + { + waypointTerrainAvoidanceSmoothingFactor = Mathf.Pow(waypointTerrainAvoidance, 0.1f); } #endregion @@ -2252,6 +2920,7 @@ void AdjustThrottle(float targetSpeed, bool useBrakes, bool allowAfterburner = t speedController.forceAfterburner = forceAfterburner; speedController.throttleOverride = throttleOverride; speedController.afterburnerPriority = ABPriority; + speedController.forceAfterburnerIfMaxThrottle = vessel.srfSpeed < ABOverrideThreshold; } Vector3 threatRelativePosition; @@ -2263,9 +2932,13 @@ void Evasive(FlightCtrlState s) if (weaponManager == null) return; SetStatus("Evading"); - debugString.AppendLine($"Evasive"); - debugString.AppendLine($"Threat Distance: {weaponManager.incomingMissileDistance}"); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) + { + debugString.AppendLine($"Evasive {evasiveTimer}s"); + debugString.AppendLine($"Threat Distance: {weaponManager.incomingMissileDistance}"); + } evading = true; + steerMode = SteerModes.NormalFlight; if (!wasEvading) evasionNonlinearityDirection = Mathf.Sign(UnityEngine.Random.Range(-1f, 1f)); bool hasABEngines = (speedController.multiModeEngines.Count > 0); @@ -2288,7 +2961,7 @@ void Evasive(FlightCtrlState s) { if ((weaponManager.ThreatClosingTime(weaponManager.incomingMissileVessel) <= 1.5f) && (!weaponManager.isChaffing)) // Missile is about to impact, pull a hard turn { - debugString.AppendLine($"Missile about to impact! pull away!"); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) debugString.AppendLine($"Missile about to impact! pull away!"); AdjustThrottle(maxSpeed, false, !weaponManager.isFlaring); @@ -2297,24 +2970,32 @@ void Evasive(FlightCtrlState s) { cross = -cross; } - FlyToPosition(s, vesselTransform.position + (50 * vessel.Velocity() / vessel.srfSpeed) + (100 * cross)); + Vector3 targetDirection = vesselTransform.position + (50 * vessel.Velocity() / vessel.srfSpeed) + (100 * cross); + RCSEvade(s, targetDirection);//add spacemode RCS dodging; missile evasion, fire in targetDirection + FlyToPosition(s, targetDirection); return; } else // Fly at 90 deg to missile to put max distance between ourselves and dispensed flares/chaff { - debugString.AppendLine($"Breaking from missile threat!"); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) debugString.AppendLine($"Breaking from missile threat!"); // Break off at 90 deg to missile Vector3 threatDirection = -1f * weaponManager.incomingMissileVessel.Velocity(); - threatDirection = Vector3.ProjectOnPlane(threatDirection, upDirection); - float sign = Vector3.SignedAngle(threatDirection, Vector3.ProjectOnPlane(vessel.Velocity(), upDirection), upDirection); - Vector3 breakDirection = Vector3.ProjectOnPlane(Vector3.Cross(Mathf.Sign(sign) * upDirection, threatDirection), upDirection); + threatDirection = threatDirection.ProjectOnPlanePreNormalized(upDirection); + float sign = Vector3.SignedAngle(threatDirection, vessel.Velocity().ProjectOnPlanePreNormalized(upDirection), upDirection); + Vector3 breakDirection = Vector3.Cross(Mathf.Sign(sign) * upDirection, threatDirection).ProjectOnPlanePreNormalized(upDirection); - // Dive to gain energy and hopefully lead missile into ground - float angle = (Mathf.Clamp((float)vessel.radarAltitude - minAltitude, 0, 1500) / 1500) * 90; - angle = Mathf.Clamp(angle, 0, 75) * Mathf.Deg2Rad; - Vector3 targetDirection = Vector3.RotateTowards(breakDirection, -upDirection, angle, 0); - targetDirection = Vector3.RotateTowards(vessel.Velocity(), targetDirection, 15f * Mathf.Deg2Rad, 0).normalized; + // Dive to gain energy and hopefully lead missile into ground when not in space + if (vessel.atmDensity > 0.05) + { + float angle = (Mathf.Clamp((float)vessel.radarAltitude - minAltitude, 0, 1500) / 1500) * 90; + float angleAdjMissile = Mathf.Max(Mathf.Asin(((float)vessel.radarAltitude - (float)weaponManager.incomingMissileVessel.radarAltitude) / + weaponManager.incomingMissileDistance) * Mathf.Rad2Deg, 0f); // Don't dive into the missile if it's coming from below + angle = Mathf.Clamp(angle - angleAdjMissile, 0, 75) * Mathf.Deg2Rad; + breakDirection = Vector3.RotateTowards(breakDirection, -upDirection, angle, 0); + } + + Vector3 targetDirection = Vector3.RotateTowards(vessel.Velocity(), breakDirection, 15f * Mathf.Deg2Rad, 0).normalized; steerMode = SteerModes.Aiming; @@ -2328,85 +3009,107 @@ void Evasive(FlightCtrlState s) useAB = true; AdjustThrottle(maxSpeed, false, useAB); } - + RCSEvade(s, targetDirection);//add spacemode RCS dodging; missile evasion, fire in targetDirection FlyToPosition(s, vesselTransform.position + (targetDirection * 100), true); return; } } else if (weaponManager.underFire) { - debugString.Append($"Dodging gunfire"); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) debugString.Append($"Dodging gunfire"); float threatDirectionFactor = Vector3.Dot(vesselTransform.up, threatRelativePosition.normalized); //Vector3 axis = -Vector3.Cross(vesselTransform.up, threatRelativePosition); - // FIXME AUBRANIUM When evading while in waypoint following mode, the breakTarget ought to be roughly in the direction of the waypoint. + // FIXME When evading while in waypoint following mode, the breakTarget ought to be roughly in the direction of the waypoint. Vector3 breakTarget = threatRelativePosition * 2f; //for the most part, we want to turn _towards_ the threat in order to increase the rel ang vel and get under its guns - if (threatDirectionFactor > 0.9f) //within 28 degrees in front - { // This adds +-500/(threat distance) to the left or right relative to the breakTarget vector, regardless of the size of breakTarget - breakTarget += 500f / threatRelativePosition.magnitude * Vector3.Cross(threatRelativePosition.normalized, Mathf.Sign(Mathf.Sin((float)vessel.missionTime / 2)) * vessel.upAxis); - debugString.AppendLine($" from directly ahead!"); + if (weaponManager.incomingThreatVessel != null && weaponManager.incomingThreatVessel.LandedOrSplashed) // Surface threat. + { + // Break horizontally away at maxAoA initially, then directly away once past 90°. + breakTarget = Vector3.RotateTowards(vessel.srf_vel_direction, -threatRelativePosition, maxAllowedAoA * Mathf.Deg2Rad, 0); + if (threatDirectionFactor > 0) + breakTarget = breakTarget.ProjectOnPlanePreNormalized(upDirection); + breakTarget = breakTarget.normalized * 100f; + var breakTargetAlt = BodyUtils.GetRadarAltitudeAtPos(vessel.transform.position + breakTarget); + if (breakTargetAlt > defaultAltitude) breakTarget -= (breakTargetAlt - defaultAltitude) * upDirection; + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) debugString.AppendLine($" from ground target."); } - else if (threatDirectionFactor < -0.9) //within ~28 degrees behind + else // Airborne threat. { - float threatDistanceSqr = threatRelativePosition.sqrMagnitude; - if (threatDistanceSqr > 400 * 400) - { // This sets breakTarget 1500m ahead and 500m down, then adds a 1000m offset at 90° to ahead based on missionTime. If the target is kinda close, brakes are also applied. - breakTarget = vesselTransform.position + vesselTransform.up * 1500 - 500 * vessel.upAxis; - breakTarget += Mathf.Sin((float)vessel.missionTime / 2) * vesselTransform.right * 1000 - Mathf.Cos((float)vessel.missionTime / 2) * vesselTransform.forward * 1000; - if (threatDistanceSqr > 800 * 800) - debugString.AppendLine($" from behind afar; engaging barrel roll"); + if (threatDirectionFactor > 0.9f) //within 28 degrees in front + { // This adds +-500/(threat distance) to the left or right relative to the breakTarget vector, regardless of the size of breakTarget + breakTarget += 500f / threatRelativePosition.magnitude * Vector3.Cross(threatRelativePosition.normalized, Mathf.Sign(Mathf.Sin((float)vessel.missionTime / 2)) * vessel.upAxis); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) debugString.AppendLine($" from directly ahead!"); + RCSEvade(s, new Vector3(1 * evasionNonlinearityDirection, 0, 0));//add spacemode RCS dodging; forward incoming fire, flank L/R + } + else if (threatDirectionFactor < -0.9) //within ~28 degrees behind + { + float threatDistanceSqr = threatRelativePosition.sqrMagnitude; + if (threatDistanceSqr > 400 * 400) + { // This sets breakTarget 1500m ahead and 500m down, then adds a 1000m offset at 90° to ahead based on missionTime. If the target is kinda close, brakes are also applied. + breakTarget = vesselTransform.up * 1500 - 500 * vessel.upAxis; + breakTarget += Mathf.Sin((float)vessel.missionTime / 2) * vesselTransform.right * 1000 - Mathf.Cos((float)vessel.missionTime / 2) * vesselTransform.forward * 1000; + if (threatDistanceSqr > 800 * 800) + { + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) debugString.AppendLine($" from behind afar; engaging barrel roll"); + } + else + { + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) debugString.AppendLine($" from behind moderate distance; engaging aggressvie barrel roll and braking"); + steerMode = SteerModes.Aiming; + AdjustThrottle(minSpeed, true, false); + } + RCSEvade(s, new Vector3(Mathf.Sin((float)vessel.missionTime / 2), Mathf.Cos((float)vessel.missionTime / 2), 0));//add spacemode RCS dodging; aft incoming fire, orbit about prograde + } else - { - debugString.AppendLine($" from behind moderate distance; engaging aggressvie barrel roll and braking"); + { // This sets breakTarget to the attackers position, then applies an up to 500m offset to the right or left (relative to the vessel) for the first half of the default evading period, then sets the breakTarget to be 150m right or left of the attacker. + breakTarget = threatRelativePosition; + if (evasiveTimer < 1.5f) + breakTarget += Mathf.Sin((float)vessel.missionTime * 2) * vesselTransform.right * 500; + else + breakTarget += -Math.Sign(Mathf.Sin((float)vessel.missionTime * 2)) * vesselTransform.right * 150; + + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) debugString.AppendLine($" from directly behind and close; breaking hard"); steerMode = SteerModes.Aiming; - AdjustThrottle(minSpeed, true, false); + AdjustThrottle(minSpeed, true, false); // Brake to slow down and turn faster while breaking target + RCSEvade(s, new Vector3(0, 0, -1));//add spacemode RCS dodging; fire available retrothrusters } } else - { // This sets breakTarget to the attackers position, then applies an up to 500m offset to the right or left (relative to the vessel) for the first half of the default evading period, then sets the breakTarget to be 150m right or left of the attacker. - breakTarget = threatRelativePosition; - if (evasiveTimer < 1.5f) - breakTarget += Mathf.Sin((float)vessel.missionTime * 2) * vesselTransform.right * 500; - else - breakTarget += -Math.Sign(Mathf.Sin((float)vessel.missionTime * 2)) * vesselTransform.right * 150; - - debugString.AppendLine($" from directly behind and close; breaking hard"); - steerMode = SteerModes.Aiming; - AdjustThrottle(minSpeed, true, false); // Brake to slow down and turn faster while breaking target - } - } - else - { - float threatDistanceSqr = threatRelativePosition.sqrMagnitude; - if (threatDistanceSqr < 400 * 400) // Within 400m to the side. - { // This sets breakTarget to be behind the attacker (relative to the evader) with a small offset to the left or right. - breakTarget += Mathf.Sin((float)vessel.missionTime * 2) * vesselTransform.right * 100; + { + float threatDistanceSqr = threatRelativePosition.sqrMagnitude; + if (threatDistanceSqr < 400 * 400) // Within 400m to the side. + { // This sets breakTarget to be behind the attacker (relative to the evader) with a small offset to the left or right. + breakTarget += Mathf.Sin((float)vessel.missionTime * 2) * vesselTransform.right * 100; - steerMode = SteerModes.Aiming; - debugString.AppendLine($" from near side; turning towards attacker"); - } - else // More than 400m to the side. - { // This sets breakTarget to be 1500m ahead, then adds a 1000m offset at 90° to ahead. - breakTarget = vesselTransform.position + vesselTransform.up * 1500; - breakTarget += Mathf.Sin((float)vessel.missionTime / 2) * vesselTransform.right * 1000 - Mathf.Cos((float)vessel.missionTime / 2) * vesselTransform.forward * 1000; - debugString.AppendLine($" from far side; engaging barrel roll"); + steerMode = SteerModes.Aiming; + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) debugString.AppendLine($" from near side; turning towards attacker"); + } + else // More than 400m to the side. + { // This sets breakTarget to be 1500m ahead, then adds a 1000m offset at 90° to ahead. + breakTarget = vesselTransform.up * 1500; + breakTarget += Mathf.Sin((float)vessel.missionTime / 2) * vesselTransform.right * 1000 - Mathf.Cos((float)vessel.missionTime / 2) * vesselTransform.forward * 1000; + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) debugString.AppendLine($" from far side; engaging barrel roll"); + RCSEvade(s, new Vector3(0, 1 * evasionNonlinearityDirection, 0));//add spacemode RCS dodging; flank incoming fire, flank U/D + } } - } - float threatAltitudeDiff = Vector3.Dot(threatRelativePosition, vessel.upAxis); - if (threatAltitudeDiff > 500) - breakTarget += threatAltitudeDiff * vessel.upAxis; //if it's trying to spike us from below, don't go crazy trying to dive below it - else - breakTarget += -150 * vessel.upAxis; //dive a bit to escape + float threatAltitudeDiff = Vector3.Dot(threatRelativePosition, vessel.upAxis); + if (threatAltitudeDiff > 500) + breakTarget += threatAltitudeDiff * vessel.upAxis; //if it's trying to spike us from below, don't go crazy trying to dive below it + else + breakTarget += -150 * vessel.upAxis; //dive a bit to escape - float breakTargetVerticalComponent = Vector3.Dot(breakTarget - vessel.transform.position, upDirection); - if (belowMinAltitude && breakTargetVerticalComponent < 0) // If we're below minimum altitude, enforce the evade direction to gain altitude. - { - breakTarget += -2f * breakTargetVerticalComponent * upDirection; + float breakTargetVerticalComponent = Vector3.Dot(breakTarget, upDirection); + if (belowMinAltitude && breakTargetVerticalComponent < 0) // If we're below minimum altitude, enforce the evade direction to gain altitude. + { + breakTarget += -2f * breakTargetVerticalComponent * upDirection; + } } - FlyToPosition(s, breakTarget); + breakTarget = GetLimitedClimbDirectionForSpeed(breakTarget); + breakTarget += vessel.transform.position; + FlyToPosition(s, FlightPosition(breakTarget, minAltitude)); return; } } @@ -2414,12 +3117,27 @@ void Evasive(FlightCtrlState s) Vector3 target = (vessel.srfSpeed < 200) ? FlightPosition(vessel.transform.position, minAltitude) : vesselTransform.position; float angleOff = Mathf.Sin(Time.time * 0.75f) * 180; angleOff = Mathf.Clamp(angleOff, -45, 45); - target += (Quaternion.AngleAxis(angleOff, upDirection) * Vector3.ProjectOnPlane(vesselTransform.up * 500, upDirection)); + target += Quaternion.AngleAxis(angleOff, upDirection) * vesselTransform.up.ProjectOnPlanePreNormalized(upDirection) * 500f; //+ (Mathf.Sin (Time.time/3) * upDirection * minAltitude/3); - debugString.AppendLine($"Evading unknown attacker"); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) debugString.AppendLine($"Evading unknown attacker"); FlyToPosition(s, target); } + public void RCSEvade(FlightCtrlState s, Vector3 EvadeDir) + { + if (!BDArmorySettings.SPACE_HACKS || vessel.atmDensity > 0.05) return; + if (!vessel.ActionGroups[KSPActionGroup.RCS]) + { + vessel.ActionGroups.SetGroup(KSPActionGroup.RCS, true); + } + //Vector3d RCS needs to be fed a vector based on the direction of dodging we need to do + // grab list of engines on ship, find all that are independant throttle, find all that are pointed in the right direction(Vector3.Dot(thrustTransform, evadeDir)? + //then activate them? Alternatively, method for letting non-ModuleRCS engines act like RCS? + s.X = Mathf.Clamp((float)EvadeDir.x, -1, 1); //left/right + s.Y = Mathf.Clamp((float)EvadeDir.z, -1, 1); //fore/aft + s.Z = Mathf.Clamp((float)EvadeDir.y, -1, 1); //up/down + } + void UpdateVelocityRelativeDirections() // Vectors that are used in TakeOff and FlyAvoidTerrain. { relativeVelocityRightDirection = Vector3.Cross(upDirection, vessel.srf_vel_direction).normalized; @@ -2439,7 +3157,7 @@ void CheckLandingGear() void TakeOff(FlightCtrlState s) { - debugString.AppendLine($"Taking off/Gaining altitude"); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) debugString.AppendLine($"Taking off/Gaining altitude"); if (vessel.LandedOrSplashed && vessel.srfSpeed < takeOffSpeed) { @@ -2451,7 +3169,7 @@ void TakeOff(FlightCtrlState s) } SetStatus("Gain Alt. (" + (int)minAltitude + "m)"); - steerMode = SteerModes.Aiming; + steerMode = initialTakeOff ? SteerModes.Aiming : SteerModes.NormalFlight; float radarAlt = (float)vessel.radarAltitude; @@ -2467,27 +3185,37 @@ void TakeOff(FlightCtrlState s) ray = new Ray(vessel.transform.position, relativeVelocityDownDirection); // Check here below. Vector3 terrainBelowNormal = (Physics.Raycast(ray, out rayHit, minAltitude + 1.0f, (int)LayerMasks.Scenery)) ? rayHit.normal : upDirection; // Terrain normal below here. Vector3 normalToUse = Vector3.Dot(vessel.srf_vel_direction, terrainBelowNormal) < Vector3.Dot(vessel.srf_vel_direction, terrainBelowAheadNormal) ? terrainBelowNormal : terrainBelowAheadNormal; // Use the normal that has the steepest slope relative to our velocity. - forwardPoint = vessel.transform.position + Vector3.ProjectOnPlane(forwardDirection, normalToUse).normalized * 100; // Forward point adjusted for terrain. - float rise = Mathf.Clamp((float)vessel.srfSpeed * 0.215f, 5, 100); // Up to 45° rise angle above terrain changes at 465m/s. - FlyToPosition(s, forwardPoint + upDirection * rise); + if (BDArmorySettings.SPACE_HACKS && vessel.atmDensity < 0.1f) //no need to worry about stalling in null atmo + { + FlyToPosition(s, vessel.transform.position + terrainBelowAheadNormal * 100); //point nose perpendicular to surface for maximum vertical thrust. + } + else + { + forwardPoint = vessel.transform.position + forwardDirection.ProjectOnPlanePreNormalized(normalToUse).normalized * 100; // Forward point adjusted for terrain. + float rise = Mathf.Clamp((float)vessel.srfSpeed * 0.215f, 5, 100); // Up to 45° rise angle above terrain changes at 465m/s. + FlyToPosition(s, forwardPoint + upDirection * rise); + } } void UpdateTerrainAlertDetectionRadius(Vessel v) { - if (v == vessel) - { - terrainAlertDetectionRadius = 2f * vessel.GetRadius(); - } + if (!HighLogic.LoadedSceneIsFlight) return; + if (v != vessel) return; + terrainAlertDetectionRadius = Mathf.Min(2f * vessel.GetRadius(), minAltitude); // Don't go above the min altitude so we're not triggering terrain avoidance while cruising at min alt. } + RaycastHit[] terrainAvoidanceHits = new RaycastHit[10]; bool FlyAvoidTerrain(FlightCtrlState s) // Check for terrain ahead. { if (initialTakeOff) return false; // Don't do anything during the initial take-off. bool initialCorrection = !avoidingTerrain; - float controlLagTime = 1.5f; // Time to fully adjust control surfaces. (Typical values seem to be 0.286s -- 1s for neutral to deployed according to wing lift comparison.) FIXME maybe this could also be a slider. + var vesselPosition = vessel.transform.position; + var vesselSrfVelDir = vessel.srf_vel_direction; + terrainAlertNormalColour = Color.green; + terrainAlertDebugRays.Clear(); ++terrainAlertTicker; - int terrainAlertTickerThreshold = BDArmorySettings.TERRAIN_ALERT_FREQUENCY * (int)(1 + Mathf.Pow((float)vessel.radarAltitude / 500.0f, 2.0f) / Mathf.Max(1.0f, (float)vessel.srfSpeed / 150.0f)); // Scale with altitude^2 / speed. + int terrainAlertTickerThreshold = BDArmorySettings.TERRAIN_ALERT_FREQUENCY * (int)(1 + ((float)(vessel.radarAltitude * vessel.radarAltitude) / 250000.0f) / Mathf.Max(1.0f, (float)vessel.srfSpeed / 150.0f)); // Scale with altitude^2 / speed. if (terrainAlertTicker >= terrainAlertTickerThreshold) { terrainAlertTicker = 0; @@ -2496,117 +3224,132 @@ void UpdateTerrainAlertDetectionRadius(Vessel v) avoidingTerrain = false; // Reset the alert. if (vessel.radarAltitude > minAltitude) belowMinAltitude = false; // Also, reset the belowMinAltitude alert if it's active because of avoiding terrain. - terrainAlertDistance = -1.0f; // Reset the terrain alert distance. + terrainAlertDistance = float.MaxValue; // Reset the terrain alert distance. float turnRadiusTwiddleFactor = turnRadiusTwiddleFactorMax; // A twiddle factor based on the orientation of the vessel, since it often takes considerable time to re-orient before avoiding the terrain. Start with the worst value. - terrainAlertThreatRange = turnRadiusTwiddleFactor * turnRadius + (float)vessel.srfSpeed * controlLagTime; // The distance to the terrain to consider. + terrainAlertThreatRange = turnRadiusTwiddleFactor * turnRadius + (float)vessel.srfSpeed * controlSurfaceDeploymentTime; // The distance to the terrain to consider. // First, look 45° down, up, left and right from our velocity direction for immediate danger. (This should cover most immediate dangers.) - Ray rayForwardUp = new Ray(vessel.transform.position, (vessel.srf_vel_direction - relativeVelocityDownDirection).normalized); - Ray rayForwardDown = new Ray(vessel.transform.position, (vessel.srf_vel_direction + relativeVelocityDownDirection).normalized); - Ray rayForwardLeft = new Ray(vessel.transform.position, (vessel.srf_vel_direction - relativeVelocityRightDirection).normalized); - Ray rayForwardRight = new Ray(vessel.transform.position, (vessel.srf_vel_direction + relativeVelocityRightDirection).normalized); + Ray rayForwardUp = new Ray(vesselPosition, (vesselSrfVelDir - relativeVelocityDownDirection).normalized); + Ray rayForwardDown = new Ray(vesselPosition, (vesselSrfVelDir + relativeVelocityDownDirection).normalized); + Ray rayForwardLeft = new Ray(vesselPosition, (vesselSrfVelDir - relativeVelocityRightDirection).normalized); + Ray rayForwardRight = new Ray(vesselPosition, (vesselSrfVelDir + relativeVelocityRightDirection).normalized); RaycastHit rayHit; - if (Physics.Raycast(rayForwardDown, out rayHit, 1.5f * terrainAlertDetectionRadius, (int)LayerMasks.Scenery)) // sqrt(2) should be sufficient, so 1.5 will cover it. + if (Physics.Raycast(rayForwardDown, out rayHit, 1.4142f * terrainAlertDetectionRadius, (int)LayerMasks.Scenery)) { - terrainAlertDistance = rayHit.distance * -Vector3.Dot(rayHit.normal, vessel.srf_vel_direction); + terrainAlertDistance = rayHit.distance * -Vector3.Dot(rayHit.normal, vesselSrfVelDir); terrainAlertNormal = rayHit.normal; + if (BDArmorySettings.DEBUG_LINES) terrainAlertDebugRays.Add(new Ray(rayHit.point, rayHit.normal)); } - if (Physics.Raycast(rayForwardUp, out rayHit, 1.5f * terrainAlertDetectionRadius, (int)LayerMasks.Scenery) && (terrainAlertDistance < 0.0f || rayHit.distance < terrainAlertDistance)) + if (Physics.Raycast(rayForwardUp, out rayHit, 1.4142f * terrainAlertDetectionRadius, (int)LayerMasks.Scenery)) { - terrainAlertDistance = rayHit.distance * -Vector3.Dot(rayHit.normal, vessel.srf_vel_direction); - terrainAlertNormal = rayHit.normal; + var distance = rayHit.distance * -Vector3.Dot(rayHit.normal, vesselSrfVelDir); + if (distance < terrainAlertDistance) + { + terrainAlertDistance = distance; + terrainAlertNormal = rayHit.normal; + } + if (BDArmorySettings.DEBUG_LINES) terrainAlertDebugRays.Add(new Ray(rayHit.point, rayHit.normal)); } - if (Physics.Raycast(rayForwardLeft, out rayHit, 1.5f * terrainAlertDetectionRadius, (int)LayerMasks.Scenery) && (terrainAlertDistance < 0.0f || rayHit.distance < terrainAlertDistance)) + if (Physics.Raycast(rayForwardLeft, out rayHit, 1.4142f * terrainAlertDetectionRadius, (int)LayerMasks.Scenery)) { - terrainAlertDistance = rayHit.distance * -Vector3.Dot(rayHit.normal, vessel.srf_vel_direction); - terrainAlertNormal = rayHit.normal; + var distance = rayHit.distance * -Vector3.Dot(rayHit.normal, vesselSrfVelDir); + if (distance < terrainAlertDistance) + { + terrainAlertDistance = distance; + terrainAlertNormal = rayHit.normal; + } + if (BDArmorySettings.DEBUG_LINES) terrainAlertDebugRays.Add(new Ray(rayHit.point, rayHit.normal)); } - if (Physics.Raycast(rayForwardRight, out rayHit, 1.5f * terrainAlertDetectionRadius, (int)LayerMasks.Scenery) && (terrainAlertDistance < 0.0f || rayHit.distance < terrainAlertDistance)) + if (Physics.Raycast(rayForwardRight, out rayHit, 1.4142f * terrainAlertDetectionRadius, (int)LayerMasks.Scenery)) { - terrainAlertDistance = rayHit.distance * -Vector3.Dot(rayHit.normal, vessel.srf_vel_direction); - terrainAlertNormal = rayHit.normal; + var distance = rayHit.distance * -Vector3.Dot(rayHit.normal, vesselSrfVelDir); + if (distance < terrainAlertDistance) + { + terrainAlertDistance = distance; + terrainAlertNormal = rayHit.normal; + } + if (BDArmorySettings.DEBUG_LINES) terrainAlertDebugRays.Add(new Ray(rayHit.point, rayHit.normal)); } - if (terrainAlertDistance > 0) + if (terrainAlertDistance < float.MaxValue) { - terrainAlertDirection = Vector3.ProjectOnPlane(vessel.srf_vel_direction, terrainAlertNormal).normalized; + terrainAlertDirection = vesselSrfVelDir.ProjectOnPlanePreNormalized(terrainAlertNormal).normalized; avoidingTerrain = true; } else { // Next, cast a sphere forwards to check for upcoming dangers. - Ray ray = new Ray(vessel.transform.position, vessel.srf_vel_direction); - if (Physics.SphereCast(ray, terrainAlertDetectionRadius, out rayHit, terrainAlertThreatRange, (int)LayerMasks.Scenery)) // Found something. + Ray ray = new Ray(vesselPosition, vesselSrfVelDir); + // For most terrain, the spherecast produces a single hit, but for buildings and special scenery (e.g., Kerbal Konstructs with multiple colliders), multiple hits are detected. + int hitCount = Physics.SphereCastNonAlloc(ray, terrainAlertDetectionRadius, terrainAvoidanceHits, terrainAlertThreatRange, (int)LayerMasks.Scenery); + if (hitCount == terrainAvoidanceHits.Length) { - // Check if there's anything directly ahead. - ray = new Ray(vessel.transform.position, vessel.srf_vel_direction); - terrainAlertDistance = rayHit.distance * -Vector3.Dot(rayHit.normal, vessel.srf_vel_direction); // Distance to terrain along direction of terrain normal. - terrainAlertNormal = rayHit.normal; - if (BDArmorySettings.DRAW_DEBUG_LINES) - { - terrainAlertDebugPos = rayHit.point; - terrainAlertDebugDir = rayHit.normal; - } - if (!Physics.Raycast(ray, out rayHit, terrainAlertThreatRange, (int)LayerMasks.Scenery)) // Nothing directly ahead, so we're just barely avoiding terrain. - { - // Change the terrain normal and direction as we want to just fly over it instead of banking away from it. - terrainAlertNormal = upDirection; - terrainAlertDirection = vessel.srf_vel_direction; - } - else - { terrainAlertDirection = Vector3.ProjectOnPlane(vessel.srf_vel_direction, terrainAlertNormal).normalized; } - float sinTheta = Math.Min(0.0f, Vector3.Dot(vessel.srf_vel_direction, terrainAlertNormal)); // sin(theta) (measured relative to the plane of the surface). - float oneMinusCosTheta = 1.0f - Mathf.Sqrt(Math.Max(0.0f, 1.0f - sinTheta * sinTheta)); - turnRadiusTwiddleFactor = (turnRadiusTwiddleFactorMin + turnRadiusTwiddleFactorMax) / 2.0f - (turnRadiusTwiddleFactorMax - turnRadiusTwiddleFactorMin) / 2.0f * Vector3.Dot(terrainAlertNormal, -vessel.transform.forward); // This would depend on roll rate (i.e., how quickly the vessel can reorient itself to perform the terrain avoidance maneuver) and probably other things. - float controlLagCompensation = Mathf.Max(0f, -Vector3.Dot(AIUtils.PredictPosition(vessel, controlLagTime * turnRadiusTwiddleFactor) - vessel.transform.position, terrainAlertNormal)); // Include twiddle factor as more re-orienting requires more control surface movement. - terrainAlertThreshold = turnRadiusTwiddleFactor * turnRadius * oneMinusCosTheta + controlLagCompensation; - if (terrainAlertDistance < terrainAlertThreshold) // Only do something about it if the estimated turn amount is a problem. - { - avoidingTerrain = true; - - // Shoot new ray in direction theta/2 (i.e., the point where we should be parallel to the surface) above velocity direction to check if the terrain slope is increasing. - float phi = -Mathf.Asin(sinTheta) / 2f; - Vector3 upcoming = Vector3.RotateTowards(vessel.srf_vel_direction, terrainAlertNormal, phi, 0f); - ray = new Ray(vessel.transform.position, upcoming); - if (BDArmorySettings.DRAW_DEBUG_LINES) - terrainAlertDebugDraw2 = false; - if (Physics.Raycast(ray, out rayHit, terrainAlertThreatRange, (int)LayerMasks.Scenery)) + terrainAvoidanceHits = Physics.SphereCastAll(ray, terrainAlertDetectionRadius, terrainAlertThreatRange, (int)LayerMasks.Scenery); + hitCount = terrainAvoidanceHits.Length; + } + if (hitCount > 0) // Found something. + { + Vector3 alertNormal = default; + using (var hits = terrainAvoidanceHits.Take(hitCount).GetEnumerator()) + while (hits.MoveNext()) { - if (rayHit.distance < terrainAlertDistance / Mathf.Sin(phi)) // Hit terrain closer than expected => terrain slope is increasing relative to our velocity direction. + var alertDistance = hits.Current.distance * -Vector3.Dot(hits.Current.normal, vesselSrfVelDir); // Distance to terrain along direction of terrain normal. + if (alertDistance < terrainAlertDistance) + { + terrainAlertDistance = alertDistance; + if (BDArmorySettings.DEBUG_LINES) terrainAlertDebugPos = hits.Current.point; + } + if (hits.Current.collider.gameObject.GetComponentUpwards() != null) // Hit a building. { - if (BDArmorySettings.DRAW_DEBUG_LINES) + // Bias building hits towards the up direction to avoid diving into terrain. + var normal = hits.Current.normal; + var hitAltitude = BodyUtils.GetRadarAltitudeAtPos(hits.Current.point); // Note: this might not work properly for Kerbal Konstructs not built at ground level. + if (hitAltitude < terrainAlertThreatRange) { - terrainAlertDebugDraw2 = true; - terrainAlertDebugPos2 = rayHit.point; - terrainAlertDebugDir2 = rayHit.normal; + normal = Vector3.RotateTowards(normal, upDirection, Mathf.Deg2Rad * 45f * (terrainAlertThreatRange - hitAltitude) / terrainAlertThreatRange, 0f); } - terrainAlertNormal = rayHit.normal; // Use the normal of the steeper terrain (relative to our velocity). - terrainAlertDirection = Vector3.ProjectOnPlane(vessel.srf_vel_direction, terrainAlertNormal).normalized; + alertNormal += normal / (1 + alertDistance * alertDistance); + if (BDArmorySettings.DEBUG_LINES) terrainAlertDebugRays.Add(new Ray(hits.Current.point, normal)); + } + else + { + alertNormal += hits.Current.normal / (1 + alertDistance * alertDistance); // Normalise multiple hits by distance^2. + if (BDArmorySettings.DEBUG_LINES) terrainAlertDebugRays.Add(new Ray(hits.Current.point, hits.Current.normal)); } } - } + terrainAlertNormal = alertNormal.normalized; + if (BDArmorySettings.DEBUG_LINES) terrainAlertDebugDir = terrainAlertNormal; + terrainAlertDirection = vesselSrfVelDir.ProjectOnPlanePreNormalized(terrainAlertNormal).normalized; + float sinTheta = Math.Min(0.0f, Vector3.Dot(vesselSrfVelDir, terrainAlertNormal)); // sin(theta) (measured relative to the plane of the surface). + float oneMinusCosTheta = 1.0f - BDAMath.Sqrt(Math.Max(0.0f, 1.0f - sinTheta * sinTheta)); + turnRadiusTwiddleFactor = (turnRadiusTwiddleFactorMin + turnRadiusTwiddleFactorMax) / 2.0f - (turnRadiusTwiddleFactorMax - turnRadiusTwiddleFactorMin) / 2.0f * Vector3.Dot(terrainAlertNormal, -vessel.transform.forward); // This would depend on roll rate (i.e., how quickly the vessel can reorient itself to perform the terrain avoidance maneuver) and probably other things. + float controlLagCompensation = Mathf.Max(0f, -Vector3.Dot(AIUtils.PredictPosition(vessel, controlSurfaceDeploymentTime) - vesselPosition, terrainAlertNormal)); // Use the same deploy time as for the threat range above. + terrainAlertThreshold = turnRadiusTwiddleFactor * turnRadius * oneMinusCosTheta + controlLagCompensation; + if (terrainAlertDistance < terrainAlertThreshold) // Only do something about it if the estimated turn amount is a problem. + avoidingTerrain = true; } } // Finally, check the distance to sea-level as water doesn't act like a collider, so it's getting ignored. Also, for planets without surfaces. if (vessel.mainBody.ocean || !vessel.mainBody.hasSolidSurface) { - float sinTheta = Vector3.Dot(vessel.srf_vel_direction, upDirection); // sin(theta) (measured relative to the ocean surface). + float sinTheta = Vector3.Dot(vesselSrfVelDir, upDirection); // sin(theta) (measured relative to the ocean surface). if (sinTheta < 0f) // Heading downwards { - float oneMinusCosTheta = 1.0f - Mathf.Sqrt(Math.Max(0.0f, 1.0f - sinTheta * sinTheta)); + float oneMinusCosTheta = 1.0f - BDAMath.Sqrt(Math.Max(0.0f, 1.0f - sinTheta * sinTheta)); turnRadiusTwiddleFactor = (turnRadiusTwiddleFactorMin + turnRadiusTwiddleFactorMax) / 2.0f - (turnRadiusTwiddleFactorMax - turnRadiusTwiddleFactorMin) / 2.0f * Vector3.Dot(upDirection, -vessel.transform.forward); // This would depend on roll rate (i.e., how quickly the vessel can reorient itself to perform the terrain avoidance maneuver) and probably other things. - float controlLagCompensation = Mathf.Max(0f, -Vector3.Dot(AIUtils.PredictPosition(vessel, controlLagTime * turnRadiusTwiddleFactor) - vessel.transform.position, upDirection)); // Include twiddle factor as more re-orienting requires more control surface movement. + float controlLagCompensation = Mathf.Max(0f, -Vector3.Dot(AIUtils.PredictPosition(vessel, controlSurfaceDeploymentTime) - vesselPosition, upDirection)); terrainAlertThreshold = turnRadiusTwiddleFactor * turnRadius * oneMinusCosTheta + controlLagCompensation; - if ((float)vessel.altitude < terrainAlertThreshold && (terrainAlertDistance < 0 || (float)vessel.altitude < terrainAlertDistance)) // If the ocean surface is closer than the terrain (if any), then override the terrain alert values. + if ((float)vessel.altitude < terrainAlertThreshold && (float)vessel.altitude < terrainAlertDistance) // If the ocean surface is closer than the terrain (if any), then override the terrain alert values. { terrainAlertDistance = (float)vessel.altitude; terrainAlertNormal = upDirection; - terrainAlertDirection = Vector3.ProjectOnPlane(vessel.srf_vel_direction, upDirection).normalized; + terrainAlertNormalColour = Color.blue; + terrainAlertDirection = vesselSrfVelDir.ProjectOnPlanePreNormalized(upDirection).normalized; avoidingTerrain = true; - if (BDArmorySettings.DRAW_DEBUG_LINES) + if (BDArmorySettings.DEBUG_LINES) { - terrainAlertDebugPos = vessel.transform.position + vessel.srf_vel_direction * (float)vessel.altitude / -sinTheta; + terrainAlertDebugPos = vesselPosition + vesselSrfVelDir * (float)vessel.altitude / -sinTheta; terrainAlertDebugDir = upDirection; } } @@ -2618,27 +3361,23 @@ void UpdateTerrainAlertDetectionRadius(Vessel v) { belowMinAltitude = true; // Inform other parts of the code to behave as if we're below minimum altitude. - float maxAngle = 70.0f * Mathf.Deg2Rad; // Maximum angle (towards surface normal) to aim. + float maxAngle = Mathf.Clamp(maxAllowedAoA, 45f, 70f) * Mathf.Deg2Rad; // Maximum angle (towards surface normal) to aim. float adjustmentFactor = 1f; // Mathf.Clamp(1.0f - Mathf.Pow(terrainAlertDistance / terrainAlertThreatRange, 2.0f), 0.0f, 1.0f); // Don't yank too hard as it kills our speed too much. (This doesn't seem necessary.) // First, aim up to maxAngle towards the surface normal. if (BDArmorySettings.SPACE_HACKS) //no need to worry about stalling in null atmo { - FlyToPosition(s, vessel.transform.position + terrainAlertNormal * 100); //so point nose perpendicular to surface for maximum vertical thrust. + FlyToPosition(s, vesselPosition + terrainAlertNormal * 100); //so point nose perpendicular to surface for maximum vertical thrust. } else { Vector3 correctionDirection = Vector3.RotateTowards(terrainAlertDirection, terrainAlertNormal, maxAngle * adjustmentFactor, 0.0f); // Then, adjust the vertical pitch for our speed (to try to avoid stalling). - Vector3 horizontalCorrectionDirection = Vector3.ProjectOnPlane(correctionDirection, upDirection).normalized; + Vector3 horizontalCorrectionDirection = correctionDirection.ProjectOnPlanePreNormalized(upDirection).normalized; correctionDirection = Vector3.RotateTowards(correctionDirection, horizontalCorrectionDirection, Mathf.Max(0.0f, (1.0f - (float)vessel.srfSpeed / 120.0f) * 0.8f * maxAngle) * adjustmentFactor, 0.0f); // Rotate up to 0.8*maxAngle back towards horizontal depending on speed < 120m/s. - float alpha = Time.fixedDeltaTime * 2f; // 0.04 seems OK. - float beta = Mathf.Pow(1.0f - alpha, terrainAlertTickerThreshold); - terrainAlertCorrectionDirection = initialCorrection ? correctionDirection : (beta * terrainAlertCorrectionDirection + (1.0f - beta) * correctionDirection).normalized; // Update our target direction over several frames (if it's not the initial correction) due to changing terrain. (Expansion of N iterations of A = A*(1-a) + B*a. Not exact due to normalisation in the loop, but good enough.) - FlyToPosition(s, vessel.transform.position + terrainAlertCorrectionDirection * 100); + FlyToPosition(s, vesselPosition + correctionDirection * 100); } // Update status and book keeping. SetStatus("Terrain (" + (int)terrainAlertDistance + "m)"); - terrainAlertCoolDown = 0.5f; // 0.5s cool down after avoiding terrain or gaining altitude. (Only used for delaying "orbitting" for now.) return true; } @@ -2653,7 +3392,7 @@ void UpdateTerrainAlertDetectionRadius(Vessel v) if (currentlyAvoidedVessel != null) // Avoidance has been triggered. { SetStatus("AvoidCollision"); - debugString.AppendLine($"Avoiding Collision"); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) debugString.AppendLine($"Avoiding Collision"); // Monitor collision avoidance, adjusting or stopping as necessary. if (currentlyAvoidedVessel != null && PredictCollisionWithVessel(currentlyAvoidedVessel, vesselCollisionAvoidanceLookAheadPeriod * 1.2f, out collisionAvoidDirection)) // *1.2f for hysteresis. @@ -2685,7 +3424,7 @@ void UpdateTerrainAlertDetectionRadius(Vessel v) if (!VesselModuleRegistry.ignoredVesselTypes.Contains(vs.Current.vesselType)) { var ibdaiControl = VesselModuleRegistry.GetModule(vs.Current); - if (ibdaiControl != null && ibdaiControl.commandLeader != null && ibdaiControl.commandLeader.vessel == vessel) continue; + if (ibdaiControl != null && ibdaiControl.currentCommand == PilotCommands.Follow && ibdaiControl.commandLeader != null && ibdaiControl.commandLeader.vessel == vessel) continue; } vesselCollision = true; collisionVesselType = vs.Current.vesselType; @@ -2716,99 +3455,81 @@ Vector3 GetLimitedClimbDirectionForSpeed(Vector3 direction) { if (Vector3.Dot(direction, upDirection) < 0) { - debugString.AppendLine($"climb limit angle: unlimited"); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) debugString.AppendLine($"climb limit angle: unlimited"); return direction; //only use this if climbing } - Vector3 planarDirection = Vector3.ProjectOnPlane(direction, upDirection).normalized * 100; + Vector3 planarDirection = direction.ProjectOnPlanePreNormalized(upDirection); float angle = Mathf.Clamp((float)vessel.srfSpeed * 0.13f, 5, 90); - debugString.AppendLine($"climb limit angle: {angle}"); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) debugString.AppendLine($"climb limit angle: {angle}"); return Vector3.RotateTowards(planarDirection, direction, angle * Mathf.Deg2Rad, 0); } void UpdateGAndAoALimits(FlightCtrlState s) { - if (vessel.dynamicPressurekPa <= 0 || vessel.srfSpeed < takeOffSpeed || belowMinAltitude && -Vector3.Dot(vessel.ReferenceTransform.forward, vessel.upAxis) < 0.8f) - { - return; - } + if (vessel.dynamicPressurekPa <= 0 || vessel.atmDensity < 0.05 || vessel.LandedOrSplashed) return; // Only measure when airborne and in sufficient atmosphere. if (lastAllowedAoA != maxAllowedAoA) { lastAllowedAoA = maxAllowedAoA; - maxAllowedCosAoA = (float)Math.Cos(lastAllowedAoA * Math.PI / 180.0); + maxAllowedSinAoA = (float)Mathf.Sin(lastAllowedAoA * Mathf.Deg2Rad); } float pitchG = -Vector3.Dot(vessel.acceleration, vessel.ReferenceTransform.forward); //should provide g force in vessel up / down direction, assuming a standard plane float pitchGPerDynPres = pitchG / (float)vessel.dynamicPressurekPa; - float curCosAoA = Vector3.Dot(vessel.Velocity().normalized, vessel.ReferenceTransform.forward); + float curSinAoA = Vector3.Dot(vessel.Velocity().normalized, vessel.ReferenceTransform.forward); //adjust moving averages - //adjust gLoad average - gLoadMovingAvg *= 32f; - gLoadMovingAvg -= gLoadMovingAvgArray[movingAvgIndex]; - gLoadMovingAvgArray[movingAvgIndex] = pitchGPerDynPres; - gLoadMovingAvg += pitchGPerDynPres; - gLoadMovingAvg /= 32f; - - //adjusting cosAoAAvg - cosAoAMovingAvg *= 32f; - cosAoAMovingAvg -= cosAoAMovingAvgArray[movingAvgIndex]; - cosAoAMovingAvgArray[movingAvgIndex] = curCosAoA; - cosAoAMovingAvg += curCosAoA; - cosAoAMovingAvg /= 32f; + smoothedGLoad.Update(pitchGPerDynPres); + var gLoad = smoothedGLoad.Value; + var gLoadPred = smoothedGLoad.At(0.1f); + if (BDArmorySettings.DEBUG_AI || BDArmorySettings.DEBUG_TELEMETRY) debugString.AppendLine($"G: {pitchG / VehiclePhysics.Gravity.reference:F1}, G-Load: current {pitchGPerDynPres:F3}, smoothed {gLoad:F3}, pred +0.1s {gLoadPred:F3} ({gLoadPred * vessel.dynamicPressurekPa / VehiclePhysics.Gravity.reference:F1}G)"); - ++movingAvgIndex; - if (movingAvgIndex == gLoadMovingAvgArray.Length) - movingAvgIndex = 0; + smoothedSinAoA.Update(curSinAoA); + var sinAoA = smoothedSinAoA.Value; + var sinAoAPred = smoothedSinAoA.At(0.1f); + if (BDArmorySettings.DEBUG_AI || BDArmorySettings.DEBUG_TELEMETRY) debugString.AppendLine($"AoA: current: {Mathf.Rad2Deg * Mathf.Asin(curSinAoA):F2}°, smoothed {Mathf.Rad2Deg * Mathf.Asin(sinAoA):F2}°, pred +0.1s {Mathf.Rad2Deg * Mathf.Asin(sinAoAPred):F2}°"); // Note: sinAoA can go beyond ±1, giving NaN in the debug line. - if (gLoadMovingAvg < maxNegG || Math.Abs(cosAoAMovingAvg - cosAoAAtMaxNegG) < 0.005f) + if (gLoadPred < maxNegG || Math.Abs(sinAoAPred - sinAoAAtMaxNegG) < 0.005f) { - maxNegG = gLoadMovingAvg; - cosAoAAtMaxNegG = cosAoAMovingAvg; + maxNegG = gLoadPred; + sinAoAAtMaxNegG = sinAoAPred; } - if (gLoadMovingAvg > maxPosG || Math.Abs(cosAoAMovingAvg - cosAoAAtMaxPosG) < 0.005f) + if (gLoadPred > maxPosG || Math.Abs(sinAoAPred - sinAoAAtMaxPosG) < 0.005f) { - maxPosG = gLoadMovingAvg; - cosAoAAtMaxPosG = cosAoAMovingAvg; + maxPosG = gLoadPred; + sinAoAAtMaxPosG = sinAoAPred; } - if (cosAoAAtMaxNegG >= cosAoAAtMaxPosG) + if (sinAoAAtMaxNegG >= sinAoAAtMaxPosG) { - cosAoAAtMaxNegG = cosAoAAtMaxPosG = maxNegG = maxPosG = 0; - gOffsetPerDynPres = gaoASlopePerDynPres = 0; + sinAoAAtMaxNegG = sinAoAAtMaxPosG = maxNegG = maxPosG = 0; + gOffsetPerDynPres = gAoASlopePerDynPres = 0; return; } - // if (maxPosG > maxDynPresGRecorded) - // maxDynPresGRecorded = maxPosG; - if (command != PilotCommands.Waypoints) // Don't decay the highest recorded G-force when following waypoints as we're likely to be heading in straight lines for longer periods. dynDynPresGRecorded *= dynDecayRate; // Decay the highest observed G-force from dynamic pressure (we want a fairly recent value in case the planes dynamics have changed). - if (!vessel.LandedOrSplashed && Math.Abs(gLoadMovingAvg) > dynDynPresGRecorded) - dynDynPresGRecorded = Math.Abs(gLoadMovingAvg); + if (!vessel.LandedOrSplashed && Math.Abs(gLoadPred) > dynDynPresGRecorded) + dynDynPresGRecorded = Math.Abs(gLoadPred); if (!vessel.LandedOrSplashed) { dynVelocityMagSqr = dynVelocityMagSqr * dynVelSmoothingCoef + (1f - dynVelSmoothingCoef) * (float)vessel.Velocity().sqrMagnitude; // Smooth the recently measured speed for determining the turn radius. } - float aoADiff = cosAoAAtMaxPosG - cosAoAAtMaxNegG; + float AoADiff = Mathf.Max(sinAoAAtMaxPosG - sinAoAAtMaxNegG, 0.001f); // Avoid divide-by-zero. - //if (Math.Abs(pitchControlDiff) < 0.005f) - // return; //if the pitch control values are too similar, don't bother to avoid numerical errors - - gaoASlopePerDynPres = (maxPosG - maxNegG) / aoADiff; - gOffsetPerDynPres = maxPosG - gaoASlopePerDynPres * cosAoAAtMaxPosG; //g force offset + gAoASlopePerDynPres = (maxPosG - maxNegG) / AoADiff; + gOffsetPerDynPres = maxPosG - gAoASlopePerDynPres * sinAoAAtMaxPosG; //g force offset } void AdjustPitchForGAndAoALimits(FlightCtrlState s) { - float minCosAoA, maxCosAoA; - //debugString += "\nMax Pos G: " + maxPosG + " @ " + cosAoAAtMaxPosG; - //debugString += "\nMax Neg G: " + maxNegG + " @ " + cosAoAAtMaxNegG; + float minSinAoA = 0, maxSinAoA = 0, curSinAoA = 0; + float negPitchDynPresLimit = 0, posPitchDynPresLimit = 0; if (vessel.LandedOrSplashed || vessel.srfSpeed < Math.Min(minSpeed, takeOffSpeed)) //if we're going too slow, don't use this { @@ -2820,135 +3541,132 @@ void AdjustPitchForGAndAoALimits(FlightCtrlState s) float invVesselDynPreskPa = 1f / (float)vessel.dynamicPressurekPa; - maxCosAoA = maxAllowedGForce * bodyGravity * invVesselDynPreskPa; - minCosAoA = -maxCosAoA; - - maxCosAoA -= gOffsetPerDynPres; - minCosAoA -= gOffsetPerDynPres; - - maxCosAoA /= gaoASlopePerDynPres; - minCosAoA /= gaoASlopePerDynPres; - - if (maxCosAoA > maxAllowedCosAoA) - maxCosAoA = maxAllowedCosAoA; - - if (minCosAoA < -maxAllowedCosAoA) - minCosAoA = -maxAllowedCosAoA; - - float curCosAoA = Vector3.Dot(vessel.Velocity() / vessel.srfSpeed, vessel.ReferenceTransform.forward); - - float centerCosAoA = (minCosAoA + maxCosAoA) * 0.5f; - float curCosAoACentered = curCosAoA - centerCosAoA; - float cosAoADiff = 0.5f * Math.Abs(maxCosAoA - minCosAoA); - float curCosAoANorm = curCosAoACentered / cosAoADiff; //scaled so that from centerAoA to maxAoA is 1 - - float negPitchScalar, posPitchScalar; - negPitchScalar = negPitchDynPresLimitIntegrator * invVesselDynPreskPa - lastPitchInput; - posPitchScalar = lastPitchInput - posPitchDynPresLimitIntegrator * invVesselDynPreskPa; - - //update pitch control limits as needed - float negPitchDynPresLimit, posPitchDynPresLimit; - negPitchDynPresLimit = posPitchDynPresLimit = 0; - if (curCosAoANorm < -0.15f)// || Math.Abs(negPitchScalar) < 0.01f) - { - float cosAoAOffset = curCosAoANorm + 1; //set max neg aoa to be 0 - float aoALimScalar = Math.Abs(curCosAoANorm); - aoALimScalar *= aoALimScalar; - aoALimScalar *= aoALimScalar; - aoALimScalar *= aoALimScalar; - if (aoALimScalar > 1) - aoALimScalar = 1; - - float pitchInputScalar = negPitchScalar; - pitchInputScalar = 1 - Mathf.Clamp01(Math.Abs(pitchInputScalar)); - pitchInputScalar *= pitchInputScalar; - pitchInputScalar *= pitchInputScalar; - pitchInputScalar *= pitchInputScalar; - if (pitchInputScalar < 0) - pitchInputScalar = 0; - - float deltaCosAoANorm = curCosAoA - lastCosAoA; - deltaCosAoANorm /= cosAoADiff; - - debugString.AppendLine($"Updating Neg Gs"); - negPitchDynPresLimitIntegrator -= 0.01f * Mathf.Clamp01(aoALimScalar + pitchInputScalar) * cosAoAOffset * (float)vessel.dynamicPressurekPa; - negPitchDynPresLimitIntegrator -= 0.005f * deltaCosAoANorm * (float)vessel.dynamicPressurekPa; - if (cosAoAOffset < 0) - negPitchDynPresLimit = -0.3f * cosAoAOffset; - } - if (curCosAoANorm > 0.15f)// || Math.Abs(posPitchScalar) < 0.01f) - { - float cosAoAOffset = curCosAoANorm - 1; //set max pos aoa to be 0 - float aoALimScalar = Math.Abs(curCosAoANorm); - aoALimScalar *= aoALimScalar; - aoALimScalar *= aoALimScalar; - aoALimScalar *= aoALimScalar; - if (aoALimScalar > 1) - aoALimScalar = 1; - - float pitchInputScalar = posPitchScalar; - pitchInputScalar = 1 - Mathf.Clamp01(Math.Abs(pitchInputScalar)); - pitchInputScalar *= pitchInputScalar; - pitchInputScalar *= pitchInputScalar; - pitchInputScalar *= pitchInputScalar; - if (pitchInputScalar < 0) - pitchInputScalar = 0; - - float deltaCosAoANorm = curCosAoA - lastCosAoA; - deltaCosAoANorm /= cosAoADiff; - - debugString.AppendLine($"Updating Pos Gs"); - posPitchDynPresLimitIntegrator -= 0.01f * Mathf.Clamp01(aoALimScalar + pitchInputScalar) * cosAoAOffset * (float)vessel.dynamicPressurekPa; - posPitchDynPresLimitIntegrator -= 0.005f * deltaCosAoANorm * (float)vessel.dynamicPressurekPa; - if (cosAoAOffset > 0) - posPitchDynPresLimit = -0.3f * cosAoAOffset; + if (maxAllowedAoA < 90) + { + maxSinAoA = maxAllowedGForce * bodyGravity * invVesselDynPreskPa; + minSinAoA = -maxSinAoA; + + maxSinAoA -= gOffsetPerDynPres; + minSinAoA -= gOffsetPerDynPres; + + maxSinAoA /= gAoASlopePerDynPres; + minSinAoA /= gAoASlopePerDynPres; + + if (maxSinAoA > maxAllowedSinAoA) + maxSinAoA = maxAllowedSinAoA; + + if (minSinAoA < -maxAllowedSinAoA) + minSinAoA = -maxAllowedSinAoA; + + curSinAoA = Vector3.Dot(vessel.Velocity().normalized, vessel.ReferenceTransform.forward); + + float centerSinAoA = (minSinAoA + maxSinAoA) * 0.5f; + float curSinAoACentered = curSinAoA - centerSinAoA; + float sinAoADiff = Mathf.Max(0.5f * Math.Abs(maxSinAoA - minSinAoA), 0.001f); // Avoid divide-by-zero. + float curSinAoANorm = curSinAoACentered / sinAoADiff; //scaled so that from centerAoA to maxAoA is 1 + + float negPitchScalar, posPitchScalar; + negPitchScalar = negPitchDynPresLimitIntegrator * invVesselDynPreskPa - lastPitchInput; + posPitchScalar = lastPitchInput - posPitchDynPresLimitIntegrator * invVesselDynPreskPa; + + //update pitch control limits as needed + negPitchDynPresLimit = posPitchDynPresLimit = 0; + if (curSinAoANorm < -0.15f) + { + float sinAoAOffset = curSinAoANorm + 1; //set max neg aoa to be 0 + float AoALimScalar = Math.Abs(curSinAoANorm); + AoALimScalar *= AoALimScalar; + AoALimScalar *= AoALimScalar; + AoALimScalar *= AoALimScalar; + if (AoALimScalar > 1) + AoALimScalar = 1; + + float pitchInputScalar = negPitchScalar; + pitchInputScalar = 1 - Mathf.Clamp01(Math.Abs(pitchInputScalar)); + pitchInputScalar *= pitchInputScalar; + pitchInputScalar *= pitchInputScalar; + pitchInputScalar *= pitchInputScalar; + if (pitchInputScalar < 0) + pitchInputScalar = 0; + + float deltaSinAoANorm = curSinAoA - lastSinAoA; + deltaSinAoANorm /= sinAoADiff; + + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) debugString.AppendLine($"Updating Neg Gs"); + negPitchDynPresLimitIntegrator -= 0.01f * Mathf.Clamp01(AoALimScalar + pitchInputScalar) * sinAoAOffset * (float)vessel.dynamicPressurekPa; + negPitchDynPresLimitIntegrator -= 0.005f * deltaSinAoANorm * (float)vessel.dynamicPressurekPa; + if (sinAoAOffset < 0) + negPitchDynPresLimit = -0.3f * sinAoAOffset; + } + if (curSinAoANorm > 0.15f) + { + float sinAoAOffset = curSinAoANorm - 1; //set max pos aoa to be 0 + float AoALimScalar = Math.Abs(curSinAoANorm); + AoALimScalar *= AoALimScalar; + AoALimScalar *= AoALimScalar; + AoALimScalar *= AoALimScalar; + if (AoALimScalar > 1) + AoALimScalar = 1; + + float pitchInputScalar = posPitchScalar; + pitchInputScalar = 1 - Mathf.Clamp01(Math.Abs(pitchInputScalar)); + pitchInputScalar *= pitchInputScalar; + pitchInputScalar *= pitchInputScalar; + pitchInputScalar *= pitchInputScalar; + if (pitchInputScalar < 0) + pitchInputScalar = 0; + + float deltaSinAoANorm = curSinAoA - lastSinAoA; + deltaSinAoANorm /= sinAoADiff; + + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) debugString.AppendLine($"Updating Pos Gs"); + posPitchDynPresLimitIntegrator -= 0.01f * Mathf.Clamp01(AoALimScalar + pitchInputScalar) * sinAoAOffset * (float)vessel.dynamicPressurekPa; + posPitchDynPresLimitIntegrator -= 0.005f * deltaSinAoANorm * (float)vessel.dynamicPressurekPa; + if (sinAoAOffset > 0) + posPitchDynPresLimit = -0.3f * sinAoAOffset; + } } float currentG = -Vector3.Dot(vessel.acceleration, vessel.ReferenceTransform.forward); float negLim, posLim; - negLim = negPitchDynPresLimitIntegrator * invVesselDynPreskPa + negPitchDynPresLimit; + negLim = vessel.atmDensity > 0.05 ? negPitchDynPresLimitIntegrator * invVesselDynPreskPa + negPitchDynPresLimit : -1; if (negLim > s.pitch) { if (currentG > -(maxAllowedGForce * 0.97f * bodyGravity)) { - negPitchDynPresLimitIntegrator -= (float)(0.15 * vessel.dynamicPressurekPa); //jsut an override in case things break + negPitchDynPresLimitIntegrator -= (float)(0.15 * vessel.dynamicPressurekPa); //just an override in case things break maxNegG = currentG * invVesselDynPreskPa; - cosAoAAtMaxNegG = curCosAoA; + sinAoAAtMaxNegG = curSinAoA; negPitchDynPresLimit = 0; - - //maxPosG = 0; - //cosAoAAtMaxPosG = 0; } - s.pitch = negLim; - debugString.AppendLine($"Limiting Neg Gs"); + SetFlightControlState(s, negLim, s.yaw, s.roll); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) debugString.AppendLine($"Limiting Neg Gs"); } - posLim = posPitchDynPresLimitIntegrator * invVesselDynPreskPa + posPitchDynPresLimit; + posLim = vessel.atmDensity > 0.05 ? posPitchDynPresLimitIntegrator * invVesselDynPreskPa + posPitchDynPresLimit : 1; if (posLim < s.pitch) { if (currentG < (maxAllowedGForce * 0.97f * bodyGravity)) { - posPitchDynPresLimitIntegrator += (float)(0.15 * vessel.dynamicPressurekPa); //jsut an override in case things break + posPitchDynPresLimitIntegrator += (float)(0.15 * vessel.dynamicPressurekPa); //just an override in case things break maxPosG = currentG * invVesselDynPreskPa; - cosAoAAtMaxPosG = curCosAoA; + sinAoAAtMaxPosG = curSinAoA; posPitchDynPresLimit = 0; - - //maxNegG = 0; - //cosAoAAtMaxNegG = 0; } - s.pitch = posLim; - debugString.AppendLine($"Limiting Pos Gs"); + SetFlightControlState(s, posLim, s.yaw, s.roll); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) debugString.AppendLine($"Limiting Pos Gs"); } lastPitchInput = s.pitch; - lastCosAoA = curCosAoA; + lastSinAoA = curSinAoA; - debugString.AppendLine(String.Format("Final Pitch: {0,7:F4} (Limits: {1,7:F4} — {2,6:F4})", s.pitch, negLim, posLim)); + // if ((BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) && negLim>posLim) debugString.AppendLine($"Bad limits: curSinAoA: {curSinAoA}, sinAoADiff: {sinAoADiff}, : curSinAoANorm: {curSinAoANorm}, maxAllowedAoA: {maxAllowedAoA}, maxAllowedSinAoA: {maxAllowedSinAoA}"); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) debugString.AppendLine(String.Format("Final Pitch: {0,7:F4} (Limits: {1,7:F4} — {2,6:F4})", s.pitch, negLim, posLim)); } void CalculateAccelerationAndTurningCircle() @@ -2958,6 +3676,32 @@ void CalculateAccelerationAndTurningCircle() maxLiftAcceleration = Mathf.Clamp(maxLiftAcceleration, bodyGravity, maxAllowedGForce * bodyGravity); //limit it to whichever is smaller, what we can provide or what we can handle. Assume minimum of 1G to avoid extremely high turn radiuses. turnRadius = dynVelocityMagSqr / maxLiftAcceleration; //radius that we can turn in assuming constant velocity, assuming simple circular motion (this is a terrible assumption, the AI usually turns on afterboosters!) + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) debugString.AppendLine($"Turn Radius: {turnRadius:G4}m (terrain threat range: {turnRadiusTwiddleFactorMax * turnRadius + (float)vessel.srfSpeed * controlSurfaceDeploymentTime:G5}m)"); + } + + void CheckFlatSpin() + { + // Checks to see if craft has a yaw rate of > 20 deg/s with pitch/roll being less (flat spin) for longer than 2s, if so sets the FlatSpin flag which will trigger + // RegainEnergy with throttle set to idle. + + float spinRate = vessel.angularVelocity.z; + if ((Mathf.Abs(spinRate) > 0.35f) && (Mathf.Abs(spinRate) > Mathf.Max(Mathf.Abs(vessel.angularVelocity.x), Mathf.Abs(vessel.angularVelocity.y)))) + { + if (flatSpinStartTime == float.MaxValue) + flatSpinStartTime = Time.time; + + if ((Time.time - flatSpinStartTime) > 2f) + { + FlatSpin = Mathf.Sign(spinRate); // 1 for counter-clockwise, -1 for clockwise + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) + debugString.AppendLine($"Flat Spin Detected, {spinRate * 180f / Mathf.PI} deg/s, {(Time.time - flatSpinStartTime)}s"); + } + } + else + { + FlatSpin = 0; // No flat spin, set to zero + flatSpinStartTime = float.MaxValue; + } } Vector3 DefaultAltPosition() @@ -2999,17 +3743,15 @@ Vector3 FlightPosition(Vector3 targetPosition, float minAlt) vertFactor += 0.15f * Mathf.Sin((float)vessel.missionTime * 0.25f); //some randomness in there - Vector3 projectedDirection = Vector3.ProjectOnPlane(forwardDirection, upDirection); - Vector3 projectedTargetDirection = Vector3.ProjectOnPlane(targetDirection, upDirection); - if (Vector3.Dot(targetDirection, forwardDirection) < 0) + Vector3 projectedDirection = forwardDirection.ProjectOnPlanePreNormalized(upDirection); + Vector3 projectedTargetDirection = targetDirection.ProjectOnPlanePreNormalized(upDirection); + var cosAngle = Vector3.Dot(targetDirection, forwardDirection); + if (cosAngle < 0) { - if (Vector3.Angle(targetDirection, forwardDirection) > 165f) - { - targetPosition = vesselTransform.position + (Quaternion.AngleAxis(Mathf.Sign(Mathf.Sin((float)vessel.missionTime / 4)) * 45, upDirection) * (projectedDirection.normalized * 200)); - targetDirection = (targetPosition - vesselTransform.position).normalized; - } + if (cosAngle < ImmelmannTurnCosAngle) + targetDirection = Vector3.RotateTowards(-vesselTransform.up, -vesselTransform.forward, Mathf.Deg2Rad * ImmelmannTurnAngle, 0); // If the target is in our blind spot, just pitch up to get a better view. (Immelmann turn.) - targetPosition = vesselTransform.position + Vector3.Cross(Vector3.Cross(forwardDirection, targetDirection), forwardDirection).normalized * 200; + targetPosition = vesselTransform.position + Vector3.Cross(Vector3.Cross(forwardDirection, targetDirection), forwardDirection).normalized * 200; // Make the target position 90° from vesselTransform.up. } else if (steerMode != SteerModes.Aiming) { @@ -3020,7 +3762,7 @@ Vector3 FlightPosition(Vector3 targetPosition, float minAlt) targetPosition += upDirection * Math.Min(distance, 1000) * vertFactor * Mathf.Clamp01(0.7f - Math.Abs(Vector3.Dot(projectedTargetDirection, projectedDirection))); if (maxAltitudeEnabled) { - var targetRadarAlt = Utils.GetRadarAltitudeAtPos(targetPosition); + var targetRadarAlt = BDArmorySettings.COMPETITION_ALTITUDE__LIMIT_ASL ? FlightGlobals.getAltitudeAtPos(targetPosition) : BodyUtils.GetRadarAltitudeAtPos(targetPosition); if (targetRadarAlt > maxAltitude) { targetPosition -= (targetRadarAlt - maxAltitude) * upDirection; @@ -3037,7 +3779,7 @@ Vector3 FlightPosition(Vector3 targetPosition, float minAlt) if (pointRadarAlt < minAlt) { float adjustment = (minAlt - pointRadarAlt); - debugString.AppendLine($"Target position is below minAlt. Adjusting by {adjustment}"); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) debugString.AppendLine($"Target position is below minAlt. Adjusting by {adjustment}"); return targetPosition + (adjustment * upDirection); } else @@ -3046,31 +3788,49 @@ Vector3 FlightPosition(Vector3 targetPosition, float minAlt) } } + Vector3 LongRangeAltitudeCorrection(Vector3 targetPosition) + { + var scale = weaponManager is not null ? Mathf.Max(2500f, weaponManager.gunRange) : 2500f; + var scaledDistance = (targetPosition - vessel.transform.position).magnitude / scale; + if (scaledDistance <= 1) return targetPosition; // No modification if the target is within the gun range. + scaledDistance = BDAMath.Sqrt(scaledDistance); + var targetAlt = BodyUtils.GetRadarAltitudeAtPos(targetPosition); + var newAlt = targetAlt / scaledDistance + defaultAltitude * (scaledDistance - 1) / scaledDistance; + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) debugString.AppendLine($"Adjusting fly-to altitude from {targetAlt:0}m to {newAlt:0}m (scaled distance: {scaledDistance:0.0}m)"); + return targetPosition + (newAlt - targetAlt) * upDirection; + } + private float SteerDamping(float angleToTarget, float defaultTargetPosition, int axis) { //adjusts steer damping relative to a vessel's angle to its target position if (!dynamicSteerDamping) // Check if enabled. { - DynamicDampingLabel = "Dyn Damping Not Toggled"; - PitchLabel = "Dyn Damping Not Toggled"; - YawLabel = "Dyn Damping Not Toggled"; - RollLabel = "Dyn Damping Not Toggled"; + if (part.PartActionWindow is not null && part.PartActionWindow.isActiveAndEnabled) + { + DynamicDampingLabel = "Dyn Damping Not Toggled"; + PitchLabel = "Dyn Damping Not Toggled"; + YawLabel = "Dyn Damping Not Toggled"; + RollLabel = "Dyn Damping Not Toggled"; + } return steerDamping; } else if (angleToTarget >= 180 || angleToTarget < 0) // Check for valid angle to target. { - if (!CustomDynamicAxisFields) - DynamicDampingLabel = "N/A"; - switch (axis) + if (part.PartActionWindow is not null && part.PartActionWindow.isActiveAndEnabled) { - case 1: - PitchLabel = "N/A"; - break; - case 2: - YawLabel = "N/A"; - break; - case 3: - RollLabel = "N/A"; - break; + if (!CustomDynamicAxisFields) + DynamicDampingLabel = "N/A"; + switch (axis) + { + case 1: + PitchLabel = "N/A"; + break; + case 2: + YawLabel = "N/A"; + break; + case 3: + RollLabel = "N/A"; + break; + } } return steerDamping; } @@ -3083,7 +3843,7 @@ private float SteerDamping(float angleToTarget, float defaultTargetPosition, int if (dynamicDampingPitch) { dynSteerDampingPitchValue = GetDampingFactor(angleToTarget, dynamicSteerDampingPitchFactor, DynamicDampingPitchMin, DynamicDampingPitchMax); - PitchLabel = dynSteerDampingPitchValue.ToString(); + if (part.PartActionWindow is not null && part.PartActionWindow.isActiveAndEnabled) PitchLabel = dynSteerDampingPitchValue.ToString(); return dynSteerDampingPitchValue; } break; @@ -3091,7 +3851,7 @@ private float SteerDamping(float angleToTarget, float defaultTargetPosition, int if (dynamicDampingYaw) { dynSteerDampingYawValue = GetDampingFactor(angleToTarget, dynamicSteerDampingYawFactor, DynamicDampingYawMin, DynamicDampingYawMax); - YawLabel = dynSteerDampingYawValue.ToString(); + if (part.PartActionWindow is not null && part.PartActionWindow.isActiveAndEnabled) YawLabel = dynSteerDampingYawValue.ToString(); return dynSteerDampingYawValue; } break; @@ -3099,31 +3859,34 @@ private float SteerDamping(float angleToTarget, float defaultTargetPosition, int if (dynamicDampingRoll) { dynSteerDampingRollValue = GetDampingFactor(angleToTarget, dynamicSteerDampingRollFactor, DynamicDampingRollMin, DynamicDampingRollMax); - RollLabel = dynSteerDampingRollValue.ToString(); + if (part.PartActionWindow is not null && part.PartActionWindow.isActiveAndEnabled) RollLabel = dynSteerDampingRollValue.ToString(); return dynSteerDampingRollValue; } break; } // The specific axis wasn't enabled, use the global value dynSteerDampingValue = steerDamping; - switch (axis) + if (part.PartActionWindow is not null && part.PartActionWindow.isActiveAndEnabled) { - case 1: - PitchLabel = dynSteerDampingValue.ToString(); - break; - case 2: - YawLabel = dynSteerDampingValue.ToString(); - break; - case 3: - RollLabel = dynSteerDampingValue.ToString(); - break; + switch (axis) + { + case 1: + PitchLabel = dynSteerDampingValue.ToString(); + break; + case 2: + YawLabel = dynSteerDampingValue.ToString(); + break; + case 3: + RollLabel = dynSteerDampingValue.ToString(); + break; + } } return dynSteerDampingValue; } else //if custom axis groups is disabled { dynSteerDampingValue = GetDampingFactor(defaultTargetPosition, dynamicSteerDampingFactor, DynamicDampingMin, DynamicDampingMax); - DynamicDampingLabel = dynSteerDampingValue.ToString(); + if (part.PartActionWindow is not null && part.PartActionWindow.isActiveAndEnabled) DynamicDampingLabel = dynSteerDampingValue.ToString(); return dynSteerDampingValue; } } @@ -3167,7 +3930,7 @@ bool DetectCollision(Vector3 direction, out Vector3 badDirection) void UpdateCommand(FlightCtrlState s) { - if (command == PilotCommands.Follow && !commandLeader) + if (command == PilotCommands.Follow && commandLeader is null) { ReleaseCommand(); return; @@ -3180,26 +3943,35 @@ void UpdateCommand(FlightCtrlState s) } else if (command == PilotCommands.FlyTo) { - SetStatus("Fly To"); - FlyOrbit(s, assignedPositionGeo, 2500, idleSpeed, ClockwiseOrbit); + if (autoTune) // Actually fly to the specified point. + { + SetStatus("AutoTuning"); + AdjustThrottle(autoTuningSpeed, true); + FlyToPosition(s, assignedPositionWorld); + } + else // Orbit around the assigned point at the default altitude. + { + SetStatus("Fly To"); + FlyOrbit(s, assignedPositionGeo, 2500, idleSpeed, ClockwiseOrbit); + } } else if (command == PilotCommands.Attack) { - if (targetVessel != null && (BDArmorySettings.RUNWAY_PROJECT || (targetVessel.vesselTransform.position - vessel.vesselTransform.position).sqrMagnitude <= weaponManager.gunRange * weaponManager.gunRange) - && (targetVessel.vesselTransform.position - vessel.vesselTransform.position).sqrMagnitude <= weaponManager.guardRange * weaponManager.guardRange) // If the vessel has a target within visual range, let it fight! + if (targetVessel != null) // && (BDArmorySettings.RUNWAY_PROJECT || (targetVessel.vesselTransform.position - vessel.vesselTransform.position).sqrMagnitude <= weaponManager.gunRange * weaponManager.gunRange) + // && (targetVessel.vesselTransform.position - vessel.vesselTransform.position).sqrMagnitude <= weaponManager.guardRange * weaponManager.guardRange) // If the vessel has a target within visual range, let it fight! { - ReleaseCommand(); + ReleaseCommand(false); return; } else if (weaponManager.underAttack || weaponManager.underFire) { - ReleaseCommand(); + ReleaseCommand(false); return; } else { SetStatus("Attack"); - FlyOrbit(s, assignedPositionGeo, 4500, maxSpeed, ClockwiseOrbit); + FlyOrbit(s, assignedPositionGeo, 2500, maxSpeed, ClockwiseOrbit); } } } @@ -3229,7 +4001,7 @@ void UpdateFollowCommand(FlightCtrlState s) flyPos = commandPosition + (ctrlModeThresh * commandHeading); Vector3 vectorToFlyPos = flyPos - vessel.ReferenceTransform.position; - Vector3 projectedPosOffset = Vector3.ProjectOnPlane(commandPosition - vessel.ReferenceTransform.position, commandHeading); + Vector3 projectedPosOffset = (commandPosition - vessel.ReferenceTransform.position).ProjectOnPlanePreNormalized(commandHeading); float posOffsetMag = projectedPosOffset.magnitude; float adjustAngle = (Mathf.Clamp(posOffsetMag * 0.27f, 0, 25)); Vector3 projVel = Vector3.Project(vessel.Velocity() - commandLeader.vessel.Velocity(), projectedPosOffset); @@ -3318,36 +4090,543 @@ protected override void OnGUI() if (!pilotEnabled || !vessel.isActiveVessel) return; - if (!BDArmorySettings.DRAW_DEBUG_LINES) return; + if (!BDArmorySettings.DEBUG_LINES) return; if (command == PilotCommands.Follow) { - BDGUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position, debugFollowPosition, 2, Color.red); + GUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position, debugFollowPosition, 2, Color.red); } - BDGUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position, debugPos, 5, Color.red); - BDGUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position, vesselTransform.position + vesselTransform.up * 1000, 3, Color.white); - BDGUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position, vesselTransform.position + -vesselTransform.forward * 100, 3, Color.yellow); - BDGUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position, vesselTransform.position + vessel.Velocity().normalized * 100, 3, Color.magenta); + GUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position, debugPos, 5, Color.red); + GUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position, vesselTransform.position + vesselTransform.up * 1000, 3, Color.white); + GUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position, vesselTransform.position + -vesselTransform.forward * 100, 3, Color.yellow); + GUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position, vesselTransform.position + vessel.Velocity().normalized * 100, 3, Color.magenta); - BDGUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position, vesselTransform.position + rollTarget, 2, Color.blue); + GUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position, vesselTransform.position + rollTarget, 2, Color.blue); #if DEBUG - if (IsEvading || IsExtending) BDGUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position, vesselTransform.position + DEBUG_vector.normalized * 10, 5, Color.cyan); + if (IsEvading || IsExtending) GUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position, vesselTransform.position + debugSquigglySquidDirection.normalized * 10, 5, Color.cyan); #endif - BDGUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position + (0.05f * vesselTransform.right), vesselTransform.position + (0.05f * vesselTransform.right) + angVelRollTarget, 2, Color.green); + GUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position + (0.05f * vesselTransform.right), vesselTransform.position + (0.05f * vesselTransform.right) + angVelRollTarget, 2, Color.green); if (avoidingTerrain) { - BDGUIUtils.DrawLineBetweenWorldPositions(vessel.transform.position, terrainAlertDebugPos, 2, Color.cyan); - BDGUIUtils.DrawLineBetweenWorldPositions(terrainAlertDebugPos, terrainAlertDebugPos + (terrainAlertThreshold - terrainAlertDistance) * terrainAlertDebugDir, 2, Color.cyan); - if (terrainAlertDebugDraw2) + GUIUtils.DrawLineBetweenWorldPositions(vessel.transform.position, terrainAlertDebugPos, 2, Color.cyan); + GUIUtils.DrawLineBetweenWorldPositions(terrainAlertDebugPos, terrainAlertDebugPos + (terrainAlertThreshold - terrainAlertDistance) * terrainAlertDebugDir, 2, Color.cyan); + GUIUtils.DrawLineBetweenWorldPositions(terrainAlertDebugPos, terrainAlertDebugPos + terrainAlertNormal * 10, 5, terrainAlertNormalColour); + foreach (var ray in terrainAlertDebugRays) GUIUtils.DrawLineBetweenWorldPositions(ray.origin, ray.origin + ray.direction * 10, 2, Color.red); + } + GUIUtils.DrawLineBetweenWorldPositions(vessel.transform.position, vessel.transform.position + 1.4142f * terrainAlertDetectionRadius * (vessel.srf_vel_direction - relativeVelocityDownDirection).normalized, 1, Color.grey); + GUIUtils.DrawLineBetweenWorldPositions(vessel.transform.position, vessel.transform.position + 1.4142f * terrainAlertDetectionRadius * (vessel.srf_vel_direction + relativeVelocityDownDirection).normalized, 1, Color.grey); + GUIUtils.DrawLineBetweenWorldPositions(vessel.transform.position, vessel.transform.position + 1.4142f * terrainAlertDetectionRadius * (vessel.srf_vel_direction - relativeVelocityRightDirection).normalized, 1, Color.grey); + GUIUtils.DrawLineBetweenWorldPositions(vessel.transform.position, vessel.transform.position + 1.4142f * terrainAlertDetectionRadius * (vessel.srf_vel_direction + relativeVelocityRightDirection).normalized, 1, Color.grey); + if (waypointTerrainAvoidanceActive) + { + GUIUtils.DrawLineBetweenWorldPositions(vessel.transform.position, waypointRayHit.point, 2, Color.cyan); // Technically, it's from 1 frame behind the current position, but close enough for visualisation. + GUIUtils.DrawLineBetweenWorldPositions(waypointRayHit.point, waypointRayHit.point + waypointTerrainSmoothedNormal * 50f, 2, Color.cyan); + } + } + } + + /// + /// A class to auto-tune the PID values of a pilot AI. + /// + /// Running with 5x time scaling once the plane is up to it's default altitude is recommended. + /// + /// Things to try: + /// - Take N samples for each direction change (ignoring the guard mode approach for now), drop outliers and average the rest to get a smoother estimate of the loss f. + /// - Sample at x-dx and x+dx to use a centred finite difference to approximate df/dx. This will require nearly twice as many samples, since we can't reuse those at x. + /// - Take dx along each axis individually instead of random directions in R^d. This would require iterating through the axes and shuffling the order each epoch or weighting them based on the size of df/dx. + /// - Build up the full gradient at each step by sampling at x±dx for each axis, then step in the direction of the gradient. + /// + public class PIDAutoTuning + { + public PIDAutoTuning(BDModulePilotAI AI) + { + this.AI = AI; + if (AI.vessel == null) { Debug.LogError($"[BDArmory.BDModulePilotAI.PIDAutoTuning]: PIDAutoTuning triggered on null vessel!"); return; } + WM = VesselModuleRegistry.GetMissileFire(AI.vessel); + partCount = AI.vessel.Parts.Count; + maxObservedSpeed = AI.idleSpeed; + } + + // External flags. + public bool measuring = false; // Whether a measurement is taking place or not. + public string vesselName = null; // Name of the vessel when auto-tuning began (in case it changes due to crashes, etc.). + + #region Internal parameters + BDModulePilotAI AI; // The AI being tuned. + MissileFire WM; // The attached WM (if trying to tune while in combat — not recommended currently). + float timeout = 15; // Measure for at most 15s. + float pointingTolerance = 0.1f; // Pointing tolerance for stopping measurements. + float rollTolerance = 5f; // Roll tolerance for stopping measurements. + float onTargetTimer = 0; + int partCount = 0; + float measurementStartTime = -1; + float measurementTime = 0; + float pointingOscillationAreaSqr = 0; + float rollOscillationAreaSqr = 0; + Vessel lastTargetVessel; + float maxObservedSpeed = 0; + float absHeadingChange = 0; + // float pitchChange = 0; + Vector3d startCoords = default; + bool recentering = false; + + #region Gradient Descent (approx) + /// + /// Learning rate scheduler. + /// This implements a ReduceLROnPlateau type of scheduler where the learning rate is reduced if no improvement in the loss occurs for a given number of steps. + /// + class LR + { + public float current = 1f; // The current learning rate. + float initial = 1f; // For resetting. + float reductionFactor = BDAMath.Sqrt(0.1f); // Two steps per order of magnitude. + int patience = 3; // Number of steps without improvement before lowering the learning rate. + int count = 0; // Count of the number of steps without improvement. + float _best = float.MaxValue; // The best result so far for the current learning rate. + public float best = float.MaxValue; // The best result so far. + + /// + /// Update the learning rate based on the current loss. + /// + /// The current loss, or some other metric. + /// True if the learning rate decreases, False otherwise. + public bool Update(float value) + { + if (value < _best) + { + _best = value; + count = 0; + if (_best < best) best = _best; + } + if (++count >= patience) + { + current *= reductionFactor; + count = 0; + _best = value; // Reset the best to avoid unnecessarily reducing the learning rate due to a fluke best score. + return true; + } + return false; + } + + /// + /// Reset everything. + /// + public void Reset(float initial) + { + this.initial = initial; + current = initial; + count = 0; + _best = float.MaxValue; + best = _best; + } + } + + /// + /// Optimise various parameters. + /// Currently, this just balances the roll relevance factor. + /// + class Optimiser + { + public float rollRelevance = 0.5f; + float initialRollRelevance = 0.5f; + float rollRelevanceMomentum = 0.8f; + + public void Update() + { + rollRelevance = rollRelevanceMomentum * rollRelevance + (1f - rollRelevanceMomentum) * Mathf.Min(_rollRelevance.Average(), 0.5f); // Clamp roll relevance to at most 0.5 in case of freak measurements. + _rollRelevance.Clear(); + } + + public void Reset() + { + rollRelevance = initialRollRelevance; + _rollRelevance.Clear(); + } + + List _rollRelevance = new List(); + public void Accumulate(float rollRelevance) + { + _rollRelevance.Add(rollRelevance); + } + } + + Dictionary fields; + HashSet fixedFields; + Dictionary baseValues; + Dictionary bestValues; + Dictionary> limits; + Dictionary>> lossSamples; // Should really use a tuple, but tuple items aren't settable. + List baseLossSamples; + bool firstCFDSample = true; + Dictionary dx; + Dictionary gradient; + List fieldNames; + string currentField = ""; + int currentFieldIndex = 0; + int sampleNumber = 0; + float headingChange = 30f; + float momentum = 0.7f; + LR lr = new LR(); + Optimiser optimiser = new Optimiser(); + #endregion + #endregion + + /// + /// Perform auto-tuning analysis. + /// + /// While measuring, this measures error^2*(α+T^2) for the pointing error and error^2*(α+T) for the roll error, where α is the "fast response relevance" and T is the measurement time. + /// This emphasises errors that don't vanish quickly, with the pointing error being more important than the roll error. + /// The final loss function is a balanced (by the optimiser) combination of the normalised pointing and roll errors. + /// + /// When between measurements, this either assigns a new fly-to point or watches for a large pointing error if guard mode is enabled (not currently recommended), and then starts a new measurement. + /// + /// + /// + /// + public void Update(float pitchError, float rollError, float yawError) + { + if (AI == null || AI.vessel == null) return; // Sanity check. + if (AI.vessel.Parts.Count < partCount) // Don't tune a plane if it's lost parts. + { + var message = $"Vessel {vesselName} has lost parts since spawning, auto-tuning disabled."; + Debug.LogWarning($"[BDArmory.BDModulePilotAI.PIDAutoTuning]: " + message); + BDACompetitionMode.Instance.competitionStatus.Add(message); + AI.AutoTune = false; + return; + } + measurementTime = Time.time - measurementStartTime; + var pointingErrorSqr = pitchError * pitchError + yawError * yawError; // Combine pitch and yaw errors as a single pointing error. + var rollErrorSqr = rollError * rollError; + if ((float)AI.vessel.srfSpeed > maxObservedSpeed) maxObservedSpeed = (float)AI.vessel.srfSpeed; + if (measuring) + { + if (pointingErrorSqr < pointingTolerance && rollErrorSqr < rollTolerance) { onTargetTimer += Time.fixedDeltaTime; } + else { onTargetTimer = 0; } + + // Measuring timed out or completed to within tolerance (on target for 0.2s if in combat, 1s outside of combat). + if (Time.time - measurementStartTime > timeout || onTargetTimer > (WM != null && WM.guardMode ? 0.2f : 1f)) + { + measurementTime = Time.time - measurementStartTime; + TakeSample(); + ResetMeasurements(); + } + else if (WM != null && WM.guardMode && WM.currentTarget != null && WM.currentTarget.Vessel != lastTargetVessel) // Target changed while in combat. Reset, but don't update PID. { - BDGUIUtils.DrawLineBetweenWorldPositions(vessel.transform.position, terrainAlertDebugPos2, 2, Color.yellow); - BDGUIUtils.DrawLineBetweenWorldPositions(terrainAlertDebugPos2, terrainAlertDebugPos2 + (terrainAlertThreshold - terrainAlertDistance) * terrainAlertDebugDir2, 2, Color.yellow); + if (BDArmorySettings.DEBUG_AI) Debug.Log($"[BDArmory.BDModulePilotAI.PIDAutoTuning]: Changed target."); + ResetMeasurements(); + } + else // Update internal parameters. + { + pointingOscillationAreaSqr += pointingErrorSqr * (AI.autoTuningOptionFastResponseRelevance + measurementTime * measurementTime); + rollOscillationAreaSqr += rollErrorSqr * (AI.autoTuningOptionFastResponseRelevance + measurementTime); // * measurementTime); // Small roll errors aren't as important as small pointing errors. } - BDGUIUtils.DrawLineBetweenWorldPositions(vessel.transform.position, vessel.transform.position + 1.5f * terrainAlertDetectionRadius * (vessel.srf_vel_direction - relativeVelocityDownDirection).normalized, 1, Color.grey); - BDGUIUtils.DrawLineBetweenWorldPositions(vessel.transform.position, vessel.transform.position + 1.5f * terrainAlertDetectionRadius * (vessel.srf_vel_direction + relativeVelocityDownDirection).normalized, 1, Color.grey); - BDGUIUtils.DrawLineBetweenWorldPositions(vessel.transform.position, vessel.transform.position + 1.5f * terrainAlertDetectionRadius * (vessel.srf_vel_direction - relativeVelocityRightDirection).normalized, 1, Color.grey); - BDGUIUtils.DrawLineBetweenWorldPositions(vessel.transform.position, vessel.transform.position + 1.5f * terrainAlertDetectionRadius * (vessel.srf_vel_direction + relativeVelocityRightDirection).normalized, 1, Color.grey); } + else if (recentering) + { + AI.CommandFlyTo((Vector3)startCoords); + if ((FlightGlobals.currentMainBody.GetWorldSurfacePosition(startCoords.x, startCoords.y, startCoords.z) - AI.vessel.transform.position).sqrMagnitude < 1e6f) // Within 1km is good enough. + { + recentering = false; + if (AI.autoTuningLossLabel.EndsWith(" re-centering")) AI.autoTuningLossLabel = AI.autoTuningLossLabel.Remove(AI.autoTuningLossLabel.Length - 15); + } + } + else + { + if (WM != null && WM.guardMode) // If guard mode is enabled, watch for target changes or something else to trigger a new measurement. This is going to be less reliable due to not using controlled fly-to directions. Don't use yet. + { + // Significantly off-target, start measuring again. + if (pointingErrorSqr > 10f) + { + if (BDArmorySettings.DEBUG_AI) Debug.Log($"[BDArmory.BDModulePilotAI.PIDAutoTuning]: Starting measuring due to being significantly off-target."); + StartMeasuring(); + } + } + else // Just cruising, assign a fly-to position and begin measuring again. + { + var upDirection = (AI.vessel.transform.position - AI.vessel.mainBody.transform.position).normalized; + var newDirection = (Quaternion.AngleAxis(headingChange, upDirection) * AI.vessel.srf_vel_direction).ProjectOnPlanePreNormalized(upDirection).normalized; + // newDirection = Quaternion.AngleAxis(pitchChange, Vector3.Cross(upDirection, newDirection)) * newDirection; + var newFlyToPoint = AI.vessel.transform.position + newDirection * maxObservedSpeed * timeout; + var altitudeAtFlyToPoint = BodyUtils.GetRadarAltitudeAtPos(newFlyToPoint, false); + var clampedAltitude = Mathf.Clamp(altitudeAtFlyToPoint, AI.autoTuningAltitude - AI.minAltitude, AI.autoTuningAltitude + AI.minAltitude); // Restrict altitude to within min altitude of the desired altitude. + newFlyToPoint += (clampedAltitude - altitudeAtFlyToPoint) * upDirection; + Vector3d flyTo; + FlightGlobals.currentMainBody.GetLatLonAlt(newFlyToPoint, out flyTo.x, out flyTo.y, out flyTo.z); + AI.CommandFlyTo((Vector3)flyTo); + StartMeasuring(); + } + } + } + + /// + /// Initialise a measurement. + /// + void StartMeasuring() + { + measuring = true; + measurementStartTime = Time.time; + if (WM != null && WM.currentTarget != null) lastTargetVessel = WM.currentTarget.Vessel; + } + + /// + /// Reset parameters used for each measurement. + /// Also, perform initial setup for auto-tuning or release the AI when finished. + /// + public void ResetMeasurements() + { + measurementStartTime = -1; + measurementTime = 0; + pointingOscillationAreaSqr = 0; + rollOscillationAreaSqr = 0; + onTargetTimer = 0; + measuring = false; + partCount = AI.vessel.Parts.Count; + + // Initial setup for auto-tuning or release the AI when finished. + if (!AI.AutoTune && AI.currentCommand == PilotCommands.FlyTo) AI.ReleaseCommand(); // Release the AI if we've been commanding it. + if (!AI.AutoTune) gradient = null; + else if (gradient == null) ResetGradient(); + } + + /// + /// Reset all the samples in preparation for the next gradient and adjust parameters that change between epochs. + /// + void ResetSamples() + { + baseLossSamples.Clear(); + lossSamples = fields.ToDictionary(kvp => kvp.Key, kvp => new List>()); + currentField = "base"; + currentFieldIndex = 0; + firstCFDSample = true; + sampleNumber = 0; + headingChange = -(30f + 0.5f * (90f / AI.autoTuningOptionNumSamples)) * Mathf.Sign(headingChange); // Initial θ for the midpoint rule approximation to ∫f(x, θ)dθ. + absHeadingChange = Mathf.Abs(headingChange); + + // Reset the dx values, taking care to avoid negative PID sample points. + dx = limits.ToDictionary(kvp => kvp.Key, kvp => Mathf.Min((AI.UpToEleven ? 0.01f : 0.1f) * BDAMath.Sqrt(lr.current) * (kvp.Value.Item2 - kvp.Value.Item1), 0.5f * baseValues[kvp.Key])); // Clamp dx when close to the minimum. + + // Update UI. + if (string.IsNullOrEmpty(AI.autoTuningLossLabel)) AI.autoTuningLossLabel = $"measuring"; + AI.autoTuningLossLabel2 = $"LR: {lr.current:G2}, Roll rel.: {optimiser.rollRelevance:G2}"; + AI.autoTuningLossLabel3 = $"{currentField}, sample nr: {sampleNumber + 1}"; + + // pitchChange = 30f * UnityEngine.Random.Range(-1f, 1f) * UnityEngine.Random.Range(-1f, 1f); // Adjust pitch by ±30°, biased towards 0°. + + if ((FlightGlobals.currentMainBody.GetWorldSurfacePosition(startCoords.x, startCoords.y, startCoords.z) - AI.vessel.transform.position).sqrMagnitude > 225e6f) // Beyond 15km should be sufficient. + { + recentering = true; + AI.autoTuningLossLabel += " re-centering"; + } + } + + /// + /// Reset everything when the auto-tuning configuration has changed (or initialised), + /// + public void ResetGradient() + { + if (!HighLogic.LoadedSceneIsFlight) return; + vesselName = AI.vessel.GetDisplayName(); + fieldNames = new List { "base" }; + fields = new Dictionary(); + fixedFields = new HashSet(); + baseValues = new Dictionary(); + gradient = new Dictionary(); + limits = new Dictionary>(); + lossSamples = new Dictionary>>(); + baseLossSamples = new List(); + bestValues = null; + + // Check which PID controls are in use and set up the required dictionaries. + foreach (var field in AI.Fields) + { + if (field.group.name == "pilotAI_PID" && field.guiActive && field.uiControlFlight.GetType() == typeof(UI_FloatRange)) + { + if (field.name.StartsWith("autoTuning")) continue; + // Exclude relevant damping fields when disabled + if (AI.dynamicSteerDamping) + { + if (((!AI.CustomDynamicAxisFields || (AI.CustomDynamicAxisFields && AI.dynamicDampingPitch && AI.dynamicDampingYaw && AI.dynamicDampingRoll)) && field.name == "steerDamping") || + (AI.CustomDynamicAxisFields && ( + (!AI.dynamicDampingPitch && (field.name.StartsWith("DynamicDampingPitch") || field.name.StartsWith("dynamicSteerDampingPitch"))) || // These fields should be named consistently! + (!AI.dynamicDampingYaw && (field.name.StartsWith("DynamicDampingYaw") || field.name.StartsWith("dynamicSteerDampingYaw"))) || // But changing them now would break old tunings. + (!AI.dynamicDampingRoll && (field.name.StartsWith("DynamicDampingRoll") || field.name.StartsWith("dynamicSteerDampingRoll"))) + ))) + { + fixedFields.Add(field.name); + continue; + } // else all damping fields shown on UI are in use + } + // Exclude fields selected by the user to be excluded. + if ((AI.autoTuningOptionFixedP && field.name == "steerMult") + || (AI.autoTuningOptionFixedI && field.name == "steerKiAdjust") + || (AI.autoTuningOptionFixedD && field.name == "steerDamping") + || (AI.autoTuningOptionFixedDOff && field.name == "DynamicDampingMin") + || (AI.autoTuningOptionFixedDOn && field.name == "DynamicDampingMax") + || (AI.autoTuningOptionFixedDF && field.name == "dynamicSteerDampingFactor") + || (AI.autoTuningOptionFixedDPOff && field.name == "DynamicDampingPitchMin") + || (AI.autoTuningOptionFixedDPOn && field.name == "DynamicDampingPitchMax") + || (AI.autoTuningOptionFixedDPF && field.name == "dynamicSteerDampingPitchFactor") + || (AI.autoTuningOptionFixedDYOff && field.name == "DynamicDampingYawMin") + || (AI.autoTuningOptionFixedDYOn && field.name == "DynamicDampingYawMax") + || (AI.autoTuningOptionFixedDYF && field.name == "dynamicSteerDampingYawFactor") + || (AI.autoTuningOptionFixedDROff && field.name == "DynamicDampingRollMin") + || (AI.autoTuningOptionFixedDROn && field.name == "DynamicDampingRollMax") + || (AI.autoTuningOptionFixedDRF && field.name == "dynamicSteerDampingRollFactor")) + { + fixedFields.Add(field.name); + continue; + } + var uiControl = (UI_FloatRange)field.uiControlFlight; + if (BDArmorySettings.DEBUG_AI) Debug.Log($"[BDArmory.BDModulePilotAI.PIDAutoTuning]: Found PID field: {field.guiName} with value {field.GetValue(AI)} and limits {uiControl.minValue} — {uiControl.maxValue}"); + fieldNames.Add(field.name); + fields.Add(field.name, field); + baseValues.Add(field.name, (float)field.GetValue(AI)); + gradient.Add(field.name, 0); + limits.Add(field.name, new Tuple(uiControl.minValue, uiControl.maxValue)); + } + } + ResetSamples(); + lr.Reset(AI.autoTuningOptionInitialLearningRate); + optimiser.Reset(); + } + + /// + /// Take a sample of the loss at the current sample position, then update internals for the next sample. + /// + void TakeSample() + { + // Measure loss at the current sample point. + var lossSample = (pointingOscillationAreaSqr / absHeadingChange + optimiser.rollRelevance * 0.01f * rollOscillationAreaSqr) / absHeadingChange; // This normalisation seems to give a roughly flat distribution over the 30°—120° range for the test craft. + optimiser.Accumulate(pointingOscillationAreaSqr / rollOscillationAreaSqr); + if (currentField == "base") + { + baseLossSamples.Add(lossSample); + if (++sampleNumber >= (int)AI.autoTuningOptionNumSamples) + { + var loss = baseLossSamples.Average(); + if (loss < lr.best) + { + bestValues = baseValues.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + Debug.Log($"[BDArmory.BDModulePilotAI.PIDAutoTuning]: Updated best values: " + string.Join(", ", bestValues.Select(kvp => fields[kvp.Key].guiName + ":" + kvp.Value)) + $", LR: {lr.current}, RR: {optimiser.rollRelevance}, Loss: {loss}"); + } + if (BDArmorySettings.DEBUG_AI) Debug.Log($"[BDArmory.BDModulePilotAI.PIDAutoTuning]: Current: " + string.Join(", ", baseValues.Select(kvp => fields[kvp.Key].guiName + ":" + kvp.Value)) + $", LR: {lr.current}, RR: {optimiser.rollRelevance}, Loss: {loss}"); + var lrDecreased = lr.Update(loss); // Update learning rate based on the current loss. + if (lrDecreased && bestValues is not null) RevertPIDValues(); // Revert to the best values when lowering the learning rate. + if (lr.current < 9e-4f) // Tuned about as far as it'll go, time to bail. (9e-4 instead of 1e-3 for some tolerance in the floating point comparison.) + { + AI.autoTuningLossLabel = $"{lr.best:G6}, completed."; + AI.AutoTune = false; // This also reverts to the best settings and stores them. + return; + } + optimiser.Update(); + AI.autoTuningLossLabel = $"{loss:G6} (best: {lr.best:G6})"; + AI.autoTuningLossLabel2 = $"LR: {lr.current:G2}, Roll rel.: {optimiser.rollRelevance:G2}"; + ++currentFieldIndex; + UpdatePIDValues(false); + sampleNumber = 0; + } + } + else + { + if (firstCFDSample) + { + lossSamples[currentField].Add(new List { lossSample }); // Sample at x - dx + firstCFDSample = false; + UpdatePIDValues(false); + } + else + { + lossSamples[currentField].Last().Add(lossSample); // Sample at x + dx + firstCFDSample = true; + if (++sampleNumber >= (int)AI.autoTuningOptionNumSamples) + { + ++currentFieldIndex; + sampleNumber = 0; + } + UpdatePIDValues((currentFieldIndex %= fieldNames.Count) == 0); + } + } + + // Change heading for next sample + headingChange = Mathf.Sign(headingChange) * (30f + (sampleNumber + 0.5f) * (90f / AI.autoTuningOptionNumSamples)); // Midpoint rule for approximation to ∫f(x, θ)dθ. + absHeadingChange = Mathf.Abs(headingChange); + + if (currentField == "base") + { AI.autoTuningLossLabel3 = $"{currentField}, sample nr: {sampleNumber + 1}"; } + else + { AI.autoTuningLossLabel3 = $"{fields[currentField].guiName}, sample nr: {sampleNumber + 1}{(firstCFDSample ? "-" : "+")}"; } + } + + /// + /// Update the PID values either for the new sample point or based on the gradient once we've got enough samples. + /// + /// + void UpdatePIDValues(bool samplingComplete) + { + if (samplingComplete) // Perform a step in the downward direction of the gradient: x -> x - lr * df/dx + { + var newGradient = lossSamples.ToDictionary(kvp => kvp.Key, kvp => lr.current * kvp.Value.Select(s => (s[1] - s[0]) / (2f * dx[kvp.Key])).Average()); // 2nd-order centred finite differences, averaged to approximate ∫f(x, θ)dθ with the domain normalised to 1 and pre-scaled by the learning rate. + foreach (var fieldName in gradient.Keys.ToList()) + { + var gradLimit = 0.1f * (limits[fieldName].Item2 - limits[fieldName].Item1); // Limit gradient changes to ±0.1 of the scale of the field. + gradient[fieldName] = gradient[fieldName] * momentum + (1f - momentum) * Mathf.Clamp(newGradient[fieldName], -gradLimit, gradLimit); // Update the gradient using momentum. + } + if (gradient.Any(kvp => float.IsNaN(kvp.Value))) + { + var message = "Gradient is giving NaN values, aborting auto-tuning."; + Debug.Log($"[BDArmory.BDModulePilotAI.PIDAutoTuning]: " + message); + BDACompetitionMode.Instance.competitionStatus.Add(message); + AI.AutoTune = false; + return; + } + if (BDArmorySettings.DEBUG_AI) Debug.Log($"[BDArmory.BDModulePilotAI.PIDAutoTuning]: Gradient: " + string.Join(", ", gradient.Select(kvp => fields[kvp.Key].guiName + ":" + kvp.Value))); + if (BDArmorySettings.DEBUG_AI) Debug.Log($"[BDArmory.BDModulePilotAI.PIDAutoTuning]: Unclamped gradient: " + string.Join(", ", newGradient.Select(kvp => fields[kvp.Key].guiName + ":" + kvp.Value))); + Dictionary absoluteGradient = new Dictionary(); + foreach (var fieldName in absoluteGradient.Keys.ToList()) absoluteGradient[fieldName] = Mathf.Abs(gradient[fieldName]); + foreach (var fieldName in baseValues.Keys.ToList()) + { + baseValues[fieldName] = baseValues[fieldName] - gradient[fieldName]; // Update PID values for gradient: x -> x - lr * df/dx. + if (AI.autoTuningOptionClampMaximums) baseValues[fieldName] = Mathf.Clamp(baseValues[fieldName], limits[fieldName].Item1, limits[fieldName].Item2); // Clamp to limits. + else baseValues[fieldName] = Mathf.Max(baseValues[fieldName], limits[fieldName].Item1); // Only clamp to the minimum. + } + foreach (var fieldName in fields.Keys.ToList()) fields[fieldName].SetValue(baseValues[fieldName], AI); // Set them in the AI. + ResetSamples(); // Reset everything for the next gradient. + } + else // Update which axis we're measuring and reset the other ones back to the base value. + { + currentField = fieldNames[currentFieldIndex]; + foreach (var fieldName in fields.Keys.ToList()) fields[fieldName].SetValue(baseValues[fieldName] + (fieldName == currentField ? (firstCFDSample ? -1f : 1f) * dx[fieldName] : 0), AI); // FIXME Sometimes these values are getting clamped by the sliders on the next Update/FixedUpdate. This doesn't seem specific to the auto-tuning though as toggling up-to-eleven was also triggering this. + } + } + + public void RevertPIDValues() + { + if (AI is null) return; + if (bestValues is not null) + { + if (BDArmorySettings.DEBUG_AI) Debug.Log($"[BDArmory.BDModulePilotAI.PIDAutoTuning]: Reverting PID values to best values: {string.Join(", ", bestValues.Select(kvp => fields[kvp.Key].guiName + ":" + kvp.Value))}"); + foreach (var fieldName in fields.Keys.ToList()) + if (bestValues.ContainsKey(fieldName)) + { + fields[fieldName].SetValue(bestValues[fieldName], AI); + if (baseValues.ContainsKey(fieldName)) // Update the base values too. + baseValues[fieldName] = bestValues[fieldName]; + } + } + else if (baseValues is not null) + { + if (BDArmorySettings.DEBUG_AI) Debug.Log($"[BDArmory.BDModulePilotAI.PIDAutoTuning]: Reverting PID values to base values: {string.Join(", ", baseValues.Select(kvp => fields[kvp.Key].guiName + ":" + kvp.Value))}"); + foreach (var fieldName in fields.Keys.ToList()) + if (baseValues.ContainsKey(fieldName)) + fields[fieldName].SetValue(baseValues[fieldName], AI); + } + } + + public void SetStartCoords() + { + if (!HighLogic.LoadedSceneIsFlight) return; + startCoords = FlightGlobals.currentMainBody.GetLatitudeAndLongitude(AI.vessel.transform.position); + startCoords.z = (float)FlightGlobals.currentMainBody.TerrainAltitude(startCoords.x, startCoords.y) + AI.autoTuningAltitude; } } } diff --git a/BDArmory/Modules/BDModuleSurfaceAI.cs b/BDArmory/Control/BDModuleSurfaceAI.cs similarity index 68% rename from BDArmory/Modules/BDModuleSurfaceAI.cs rename to BDArmory/Control/BDModuleSurfaceAI.cs index f080b8e38..f8db47341 100644 --- a/BDArmory/Modules/BDModuleSurfaceAI.cs +++ b/BDArmory/Control/BDModuleSurfaceAI.cs @@ -3,14 +3,16 @@ using System.Collections.Generic; using System.Linq; using System.Text; -using BDArmory.Control; -using BDArmory.Core; -using BDArmory.Core.Extension; -using BDArmory.Misc; -using BDArmory.UI; using UnityEngine; -namespace BDArmory.Modules +using BDArmory.Extensions; +using BDArmory.Settings; +using BDArmory.UI; +using BDArmory.Utils; +using BDArmory.Weapons; +using BDArmory.Weapons.Missiles; + +namespace BDArmory.Control { public class BDModuleSurfaceAI : BDGenericAIBase, IBDAIControl { @@ -34,7 +36,7 @@ public class BDModuleSurfaceAI : BDGenericAIBase, IBDAIControl Vector3 upDir; AIUtils.TraversabilityMatrix pathingMatrix; - List waypoints = new List(); + List pathingWaypoints = new List(); bool leftPath = false; protected override Vector3d assignedPositionGeo @@ -185,6 +187,7 @@ public override void OnStart(StartState state) { base.OnStart(state); SetChooseOptions(); + ChooseOptionsUpdated(null, null); } public override void ActivatePilot() @@ -233,6 +236,28 @@ public void SetChooseOptions() public void ChooseOptionsUpdated(BaseField field, object obj) { + // Hide/display the AI fields + var fieldEnabled = SurfaceType != AIUtils.VehicleMovementType.Stationary; + foreach (var fieldName in new List{ + "MaxSlopeAngle", + "CruiseSpeed", + "MaxSpeed", + "MaxDrift", + "TargetPitch", + "BankAngle", + // "steerMult", + // "steerDamping", + "BroadsideAttack", + // "MinEngagementRange", + // "MaxEngagementRange", + // "ManeuverRCS", + "AvoidMass", + "OrbitDirectionName" + }) + { + Fields[fieldName].guiActive = fieldEnabled; + Fields[fieldName].guiActiveEditor = fieldEnabled; + } this.part.RefreshAssociatedWindows(); if (BDArmoryAIGUI.Instance != null) { @@ -282,16 +307,17 @@ protected override void OnGUI() if (!pilotEnabled || !vessel.isActiveVessel) return; - if (!BDArmorySettings.DRAW_DEBUG_LINES) return; + if (!BDArmorySettings.DEBUG_LINES) return; if (command == PilotCommands.Follow) { - BDGUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position, assignedPositionWorld, 2, Color.red); + GUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position, assignedPositionWorld, 2, Color.red); } - BDGUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position, vesselTransform.position + targetDirection * 10f, 2, Color.blue); - BDGUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position + (0.05f * vesselTransform.right), vesselTransform.position + (0.05f * vesselTransform.right), 2, Color.green); + GUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position, vesselTransform.position + targetDirection * 10f, 2, Color.blue); + GUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position + (0.05f * vesselTransform.right), vesselTransform.position + (0.05f * vesselTransform.right), 2, Color.green); - pathingMatrix.DrawDebug(vessel.CoM, waypoints); + if (SurfaceType != AIUtils.VehicleMovementType.Stationary) + pathingMatrix.DrawDebug(vessel.CoM, pathingWaypoints); } #endregion events @@ -307,10 +333,10 @@ protected override void AutoPilot(FlightCtrlState s) targetDirection = vesselTransform.up; aimingMode = false; upDir = VectorUtils.GetUpDirection(vesselTransform.position); - DebugLine(""); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) DebugLine(""); // check if we should be panicking - if (!PanicModes()) + if (SurfaceType == AIUtils.VehicleMovementType.Stationary || !PanicModes()) // Stationary vehicles don't panic (so, free-fall stationary turrets are a possibility). { // pilot logic figures out what we're supposed to be doing, and sets the base state PilotLogic(); @@ -324,40 +350,44 @@ protected override void AutoPilot(FlightCtrlState s) void PilotLogic() { - // check for collisions, but not every frame - if (collisionDetectionTicker == 0) + if (SurfaceType != AIUtils.VehicleMovementType.Stationary) { - collisionDetectionTicker = 20; - float predictMult = Mathf.Clamp(10 / MaxDrift, 1, 10); + // check for collisions, but not every frame + if (collisionDetectionTicker == 0) + { + collisionDetectionTicker = 20; + float predictMult = Mathf.Clamp(10 / MaxDrift, 1, 10); - dodgeVector = null; + dodgeVector = null; - using (var vs = BDATargetManager.LoadedVessels.GetEnumerator()) - while (vs.MoveNext()) - { - if (vs.Current == null || vs.Current == vessel || vs.Current.GetTotalMass() < AvoidMass) continue; - if (!VesselModuleRegistry.ignoredVesselTypes.Contains(vs.Current.vesselType)) + using (var vs = BDATargetManager.LoadedVessels.GetEnumerator()) + while (vs.MoveNext()) { - var ibdaiControl = VesselModuleRegistry.GetModule(vs.Current); - if (!vs.Current.LandedOrSplashed || (ibdaiControl != null && ibdaiControl.commandLeader != null && ibdaiControl.commandLeader.vessel == vessel)) - continue; + if (vs.Current == null || vs.Current == vessel || vs.Current.GetTotalMass() < AvoidMass) continue; + if (!VesselModuleRegistry.ignoredVesselTypes.Contains(vs.Current.vesselType)) + { + var ibdaiControl = VesselModuleRegistry.GetModule(vs.Current); + if (!vs.Current.LandedOrSplashed || (ibdaiControl != null && ibdaiControl.commandLeader != null && ibdaiControl.commandLeader.vessel == vessel)) + continue; + } + dodgeVector = PredictCollisionWithVessel(vs.Current, 5f * predictMult, 0.5f); + if (dodgeVector != null) break; } - dodgeVector = PredictCollisionWithVessel(vs.Current, 5f * predictMult, 0.5f); - if (dodgeVector != null) break; - } - } - else - collisionDetectionTicker--; + } + else + collisionDetectionTicker--; - // avoid collisions if any are found - if (dodgeVector != null) - { - targetVelocity = PoweredSteering ? MaxSpeed : CruiseSpeed; - targetDirection = (Vector3)dodgeVector; - SetStatus($"Avoiding Collision"); - leftPath = true; - return; + // avoid collisions if any are found + if (dodgeVector != null) + { + targetVelocity = PoweredSteering ? MaxSpeed : CruiseSpeed; + targetDirection = (Vector3)dodgeVector; + SetStatus($"Avoiding Collision"); + leftPath = true; + return; + } } + else { collisionDetectionTicker = 0; } // if bypass target is no longer relevant, remove it if (bypassTarget != null && ((bypassTarget != targetVessel && bypassTarget != (commandLeader != null ? commandLeader.vessel : null)) @@ -382,9 +412,23 @@ void PilotLogic() float shotSpeed = 1000f; if ((weaponManager != null ? weaponManager.selectedWeapon : null) is ModuleWeapon wep) shotSpeed = wep.bulletVelocity; - vecToTarget = targetVessel.PredictPosition(distance / shotSpeed) - vessel.CoM; + var timeToCPA = targetVessel.TimeToCPA(vessel.CoM, vessel.Velocity() + vesselTransform.up * shotSpeed, FlightGlobals.getGeeForceAtPosition(vessel.CoM), MaxEngagementRange / shotSpeed); + vecToTarget = targetVessel.PredictPosition(timeToCPA) - vessel.CoM; - if (BroadsideAttack) + if (SurfaceType == AIUtils.VehicleMovementType.Stationary) + { + if (distance >= MinEngagementRange && distance <= MaxEngagementRange) + { + targetDirection = vecToTarget; + aimingMode = true; + } + else + { + SetStatus("On Alert"); + return; + } + } + else if (BroadsideAttack) { Vector3 sideVector = Vector3.Cross(vecToTarget, upDir); //find a vector perpendicular to direction to target if (collisionDetectionTicker == 10 @@ -400,11 +444,11 @@ void PilotLogic() (MaxEngagementRange - distance) / (MaxEngagementRange - MinEngagementRange) * (1 - AttackAngleAtMaxRange / 90) + AttackAngleAtMaxRange / 90); // attackAngle to 90 degrees from maxrange to minrange targetDirection = Vector3.LerpUnclamped(vecToTarget.normalized, sideVector.normalized, sidestep); // interpolate between the side vector and target direction vector based on sidestep targetVelocity = MaxSpeed; - DebugLine($"Broadside attack angle {sidestep}"); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) DebugLine($"Broadside attack angle {sidestep}"); } else // just point at target and go { - if ((targetVessel.horizontalSrfSpeed < 10 || Vector3.Dot(Vector3.ProjectOnPlane(targetVessel.srf_vel_direction, upDir), vessel.up) < 0) //if target is stationary or we're facing in opposite directions + if ((targetVessel.horizontalSrfSpeed < 10 || Vector3.Dot(targetVessel.srf_vel_direction.ProjectOnPlanePreNormalized(upDir), vessel.up) < 0) //if target is stationary or we're facing in opposite directions && (distance < MinEngagementRange || (distance < (MinEngagementRange * 3 + MaxEngagementRange) / 4 //and too close together && extendingTarget != null && targetVessel != null && extendingTarget == targetVessel))) { @@ -418,7 +462,7 @@ void PilotLogic() else { extendingTarget = null; - targetDirection = Vector3.ProjectOnPlane(vecToTarget, upDir); + targetDirection = vecToTarget.ProjectOnPlanePreNormalized(upDir); if (Vector3.Dot(targetDirection, vesselTransform.up) < 0) targetVelocity = PoweredSteering ? MaxSpeed : 0; // if facing away from target else if (distance >= MaxEngagementRange || distance <= MinEngagementRange) @@ -434,7 +478,7 @@ void PilotLogic() case WeaponClasses.Rocket: case WeaponClasses.DefenseLaser: var gun = (ModuleWeapon)weaponManager.selectedWeapon; - if ((gun.yawRange == 0 || gun.maxPitch == gun.minPitch) && gun.FiringSolutionVector != null) + if (gun != null && (gun.yawRange == 0 || gun.maxPitch == gun.minPitch) && gun.FiringSolutionVector != null) { aimingMode = true; if (Vector3.Angle((Vector3)gun.FiringSolutionVector, vessel.transform.up) < 20) @@ -452,7 +496,7 @@ void PilotLogic() } // follow - if (command == PilotCommands.Follow) + if (command == PilotCommands.Follow && SurfaceType != AIUtils.VehicleMovementType.Stationary) { leftPath = true; if (collisionDetectionTicker == 5) @@ -461,14 +505,14 @@ void PilotLogic() Vector3 targetPosition = GetFormationPosition(); Vector3 targetDistance = targetPosition - vesselTransform.position; if (Vector3.Dot(targetDistance, vesselTransform.up) < 0 - && Vector3.ProjectOnPlane(targetDistance, upDir).sqrMagnitude < 250f * 250f + && targetDistance.ProjectOnPlanePreNormalized(upDir).sqrMagnitude < 250f * 250f && Vector3.Angle(vesselTransform.up, commandLeader.vessel.srf_velocity) < 0.8f) { - targetDirection = Vector3.RotateTowards(Vector3.ProjectOnPlane(commandLeader.vessel.srf_vel_direction, upDir), targetDistance, 0.2f, 0); + targetDirection = Vector3.RotateTowards(commandLeader.vessel.srf_vel_direction.ProjectOnPlanePreNormalized(upDir), targetDistance, 0.2f, 0); } else { - targetDirection = Vector3.ProjectOnPlane(targetDistance, upDir); + targetDirection = targetDistance.ProjectOnPlanePreNormalized(upDir); } targetVelocity = (float)(commandLeader.vessel.horizontalSrfSpeed + (vesselTransform.position - targetPosition).magnitude / 15); if (Vector3.Dot(targetDirection, vesselTransform.up) < 0 && !PoweredSteering) targetVelocity = 0; @@ -477,32 +521,35 @@ void PilotLogic() } } - // goto - if (leftPath && bypassTarget == null) + if (SurfaceType != AIUtils.VehicleMovementType.Stationary) { - Pathfind(finalPositionGeo); - leftPath = false; - } + // goto + if (leftPath && bypassTarget == null) + { + Pathfind(finalPositionGeo); + leftPath = false; + } - const float targetRadius = 250f; - targetDirection = Vector3.ProjectOnPlane(assignedPositionWorld - vesselTransform.position, upDir); + const float targetRadius = 250f; + targetDirection = (assignedPositionWorld - vesselTransform.position).ProjectOnPlanePreNormalized(upDir); - if (targetDirection.sqrMagnitude > targetRadius * targetRadius) - { - if (bypassTarget != null) - targetVelocity = MaxSpeed; - else if (waypoints.Count > 1) - targetVelocity = command == PilotCommands.Attack ? MaxSpeed : CruiseSpeed; - else - targetVelocity = Mathf.Clamp((targetDirection.magnitude - targetRadius / 2) / 5f, - 0, command == PilotCommands.Attack ? MaxSpeed : CruiseSpeed); + if (targetDirection.sqrMagnitude > targetRadius * targetRadius) + { + if (bypassTarget != null) + targetVelocity = MaxSpeed; + else if (pathingWaypoints.Count > 1) + targetVelocity = command == PilotCommands.Attack ? MaxSpeed : CruiseSpeed; + else + targetVelocity = Mathf.Clamp((targetDirection.magnitude - targetRadius / 2) / 5f, + 0, command == PilotCommands.Attack ? MaxSpeed : CruiseSpeed); - if (Vector3.Dot(targetDirection, vesselTransform.up) < 0 && !PoweredSteering) targetVelocity = 0; - SetStatus(bypassTarget ? "Repositioning" : "Moving"); - return; - } + if (Vector3.Dot(targetDirection, vesselTransform.up) < 0 && !PoweredSteering) targetVelocity = 0; + SetStatus(bypassTarget ? "Repositioning" : "Moving"); + return; + } - cycleWaypoint(); + cycleWaypoint(); + } SetStatus($"Not doing anything in particular"); targetDirection = vesselTransform.up; @@ -516,7 +563,7 @@ void Tactical() || weaponManager.underFire || weaponManager.missileIsIncoming); // if weaponManager thinks we're under fire, do the evasive dance - if (weaponManager.underFire || weaponManager.missileIsIncoming) + if (SurfaceType != AIUtils.VehicleMovementType.Stationary && (weaponManager.underFire || weaponManager.missileIsIncoming)) { targetVelocity = MaxSpeed; if (weaponManager.underFire || weaponManager.incomingMissileDistance < 2500) @@ -533,7 +580,7 @@ void Tactical() { weaveAdjustment = 0; } - DebugLine($"underFire {weaponManager.underFire}, weaveAdjustment {weaveAdjustment}"); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) DebugLine($"underFire {weaponManager.underFire}, weaveAdjustment {weaveAdjustment}"); } bool PanicModes() @@ -541,7 +588,7 @@ bool PanicModes() if (!vessel.LandedOrSplashed && !BDArmorySettings.SF_REPULSOR) { targetVelocity = 0; - targetDirection = Vector3.ProjectOnPlane(vessel.srf_velocity, upDir); + targetDirection = vessel.srf_velocity.ProjectOnPlanePreNormalized(upDir); SetStatus("Airtime!"); return true; } @@ -569,11 +616,13 @@ void AdjustThrottle(float targetSpeed) if (float.IsNaN(targetSpeed)) //because yeah, I might have left division by zero in there somewhere { targetSpeed = CruiseSpeed; - DebugLine("Target velocity NaN, set to CruiseSpeed."); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) DebugLine("Target velocity NaN, set to CruiseSpeed."); } else - DebugLine($"Target velocity: {targetVelocity}"); - DebugLine($"engine thrust: {speedController.debugThrust}, motor zero: {motorControl.zeroPoint}"); + { + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) DebugLine($"Target velocity: {targetVelocity}"); + } + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) DebugLine($"engine thrust: {speedController.debugThrust}, motor zero: {motorControl.zeroPoint}"); speedController.targetSpeed = motorControl.targetSpeed = targetSpeed; speedController.useBrakes = motorControl.preventNegativeZeroPoint = speedController.debugThrust > 0; @@ -583,70 +632,114 @@ void AttitudeControl(FlightCtrlState s) { const float terrainOffset = 5; - Vector3 yawTarget = Vector3.ProjectOnPlane(targetDirection, vesselTransform.forward); + Vector3 yawTarget = targetDirection.ProjectOnPlanePreNormalized(vesselTransform.forward); // limit "aoa" if we're moving float driftMult = 1; - if (vessel.horizontalSrfSpeed * 10 > CruiseSpeed) + if (SurfaceType != AIUtils.VehicleMovementType.Stationary && vessel.horizontalSrfSpeed * 10 > CruiseSpeed) { driftMult = Mathf.Max(Vector3.Angle(vessel.srf_velocity, yawTarget) / MaxDrift, 1); yawTarget = Vector3.RotateTowards(vessel.srf_velocity, yawTarget, MaxDrift * Mathf.Deg2Rad, 0); } float yawError = VectorUtils.SignedAngle(vesselTransform.up, yawTarget, vesselTransform.right) + (aimingMode ? 0 : weaveAdjustment); - DebugLine($"yaw target: {yawTarget}, yaw error: {yawError}"); - DebugLine($"drift multiplier: {driftMult}"); - - Vector3 baseForward = vessel.transform.up * terrainOffset; - float basePitch = Mathf.Atan2( - AIUtils.GetTerrainAltitude(vessel.CoM + baseForward, vessel.mainBody, false) - - AIUtils.GetTerrainAltitude(vessel.CoM - baseForward, vessel.mainBody, false), - terrainOffset * 2) * Mathf.Rad2Deg; - float pitchAngle = basePitch + TargetPitch * Mathf.Clamp01((float)vessel.horizontalSrfSpeed / CruiseSpeed); - if (aimingMode) - pitchAngle = VectorUtils.SignedAngle(vesselTransform.up, Vector3.ProjectOnPlane(targetDirection, vesselTransform.right), -vesselTransform.forward); - DebugLine($"terrain fw slope: {basePitch}, target pitch: {pitchAngle}"); - - float pitch = 90 - Vector3.Angle(vesselTransform.up, upDir); - float pitchError = pitchAngle - pitch; - - Vector3 baseLateral = vessel.transform.right * terrainOffset; - float baseRoll = Mathf.Atan2( - AIUtils.GetTerrainAltitude(vessel.CoM + baseLateral, vessel.mainBody, false) - - AIUtils.GetTerrainAltitude(vessel.CoM - baseLateral, vessel.mainBody, false), - terrainOffset * 2) * Mathf.Rad2Deg; - float drift = VectorUtils.SignedAngle(vesselTransform.up, Vector3.ProjectOnPlane(vessel.GetSrfVelocity(), upDir), vesselTransform.right); - float bank = VectorUtils.SignedAngle(-vesselTransform.forward, upDir, -vesselTransform.right); - float targetRoll = baseRoll + BankAngle * Mathf.Clamp01(drift / MaxDrift) * Mathf.Clamp01((float)vessel.srfSpeed / CruiseSpeed); - float rollError = targetRoll - bank; - DebugLine($"terrain sideways slope: {baseRoll}, target roll: {targetRoll}"); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) + { + DebugLine($"yaw target: {yawTarget}, yaw error: {yawError}"); + DebugLine($"drift multiplier: {driftMult}"); + } + + float pitchError = 0; + if (SurfaceType != AIUtils.VehicleMovementType.Stationary) + { + Vector3 baseForward = vessel.transform.up * terrainOffset; + float basePitch = Mathf.Atan2( + AIUtils.GetTerrainAltitude(vessel.CoM + baseForward, vessel.mainBody, false) + - AIUtils.GetTerrainAltitude(vessel.CoM - baseForward, vessel.mainBody, false), + terrainOffset * 2) * Mathf.Rad2Deg; + float pitchAngle = basePitch + TargetPitch * Mathf.Clamp01((float)vessel.horizontalSrfSpeed / CruiseSpeed); + if (aimingMode) + pitchAngle = VectorUtils.SignedAngle(vesselTransform.up, targetDirection.ProjectOnPlanePreNormalized(vesselTransform.right), -vesselTransform.forward); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) DebugLine($"terrain fw slope: {basePitch}, target pitch: {pitchAngle}"); + float pitch = 90 - Vector3.Angle(vesselTransform.up, upDir); + pitchError = pitchAngle - pitch; + } + else + { + pitchError = VectorUtils.SignedAngle(vesselTransform.up, targetDirection.ProjectOnPlanePreNormalized(vesselTransform.right), -vesselTransform.forward); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) DebugLine($"pitch error: {pitchError}"); + } + + + float rollError = 0; + if (SurfaceType != AIUtils.VehicleMovementType.Stationary) + { + Vector3 baseLateral = vessel.transform.right * terrainOffset; + float baseRoll = Mathf.Atan2( + AIUtils.GetTerrainAltitude(vessel.CoM + baseLateral, vessel.mainBody, false) + - AIUtils.GetTerrainAltitude(vessel.CoM - baseLateral, vessel.mainBody, false), + terrainOffset * 2) * Mathf.Rad2Deg; + float drift = VectorUtils.SignedAngle(vesselTransform.up, vessel.GetSrfVelocity().ProjectOnPlanePreNormalized(upDir), vesselTransform.right); + float bank = VectorUtils.SignedAngle(-vesselTransform.forward, upDir, -vesselTransform.right); + float targetRoll = baseRoll + BankAngle * Mathf.Clamp01(drift / MaxDrift) * Mathf.Clamp01((float)vessel.srfSpeed / CruiseSpeed); + rollError = targetRoll - bank; + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) DebugLine($"terrain sideways slope: {baseRoll}, target roll: {targetRoll}"); + } + else + { + rollError = VectorUtils.SignedAngle(-vesselTransform.forward, upDir, vesselTransform.right); + } Vector3 localAngVel = vessel.angularVelocity; - s.roll = steerMult * 0.006f * rollError - 0.4f * steerDamping * -localAngVel.y; - s.pitch = ((aimingMode ? 0.02f : 0.015f) * steerMult * pitchError) - (steerDamping * -localAngVel.x); - s.yaw = (((aimingMode ? 0.007f : 0.005f) * steerMult * yawError) - (steerDamping * 0.2f * -localAngVel.z)) * driftMult; - s.wheelSteer = -(((aimingMode ? 0.005f : 0.003f) * steerMult * yawError) - (steerDamping * 0.1f * -localAngVel.z)); + SetFlightControlState(s, + ((aimingMode ? 0.02f : 0.015f) * steerMult * pitchError) - (steerDamping * -localAngVel.x), // pitch + (((aimingMode ? 0.007f : 0.005f) * steerMult * yawError) - (steerDamping * 0.2f * -localAngVel.z)) * driftMult, // yaw + steerMult * 0.006f * rollError - 0.4f * steerDamping * -localAngVel.y, // roll + -(((aimingMode ? 0.005f : 0.003f) * steerMult * yawError) - (steerDamping * 0.1f * -localAngVel.z)) // wheel steer + ); if (ManeuverRCS && (Mathf.Abs(s.roll) >= 1 || Mathf.Abs(s.pitch) >= 1 || Mathf.Abs(s.yaw) >= 1)) vessel.ActionGroups.SetGroup(KSPActionGroup.RCS, true); } + protected void SetFlightControlState(FlightCtrlState s, float pitch, float yaw, float roll, float wheelSteer) + { + base.SetFlightControlState(s, pitch, yaw, roll); + s.wheelSteer = wheelSteer; + if (hasAxisGroupsModule) + { + axisGroupsModule.UpdateAxisGroup(KSPAxisGroup.WheelSteer, wheelSteer); + } + + } #endregion Actual AI Pilot #region Autopilot helper functions public override bool CanEngage() { - if (vessel.Splashed && (SurfaceType & AIUtils.VehicleMovementType.Water) == 0) - DebugLine(vessel.vesselName + " cannot engage: boat not in water"); + if (SurfaceType == AIUtils.VehicleMovementType.Stationary) // Stationary can shoot at whatever it can see without moving. + { + return true; + } + else if (vessel.Splashed && (SurfaceType & AIUtils.VehicleMovementType.Water) == 0) + { + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) DebugLine(vessel.vesselName + " cannot engage: boat not in water"); + } else if (vessel.Landed && (SurfaceType & AIUtils.VehicleMovementType.Land) == 0) - DebugLine(vessel.vesselName + " cannot engage: vehicle not on land"); + { + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) DebugLine(vessel.vesselName + " cannot engage: vehicle not on land"); + } else if (!vessel.LandedOrSplashed) - DebugLine(vessel.vesselName + " cannot engage: vessel not on surface"); + { + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) DebugLine(vessel.vesselName + " cannot engage: vessel not on surface"); + } // the motorControl part fails sometimes, and guard mode then decides not to select a weapon // figure out what is wrong with motor control before uncommenting :D - //else if (speedController.debugThrust + (motorControl?.MaxAccel ?? 0) <= 0) - // DebugLine(vessel.vesselName + " cannot engage: no engine power"); + // else if (speedController.debugThrust + (motorControl?.MaxAccel ?? 0) <= 0) + // { + // if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) DebugLine(vessel.vesselName + " cannot engage: no engine power"); + // } else return true; return false; @@ -692,14 +785,14 @@ void checkBypass(Vessel target) { bypassTarget = target; bypassTargetPos = VectorUtils.WorldPositionToGeoCoords(target.CoM, vessel.mainBody); - waypoints = pathingMatrix.Pathfind( + pathingWaypoints = pathingMatrix.Pathfind( VectorUtils.WorldPositionToGeoCoords(vessel.CoM, vessel.mainBody), VectorUtils.WorldPositionToGeoCoords(target.CoM, vessel.mainBody), vessel.mainBody, SurfaceType, MaxSlopeAngle, AvoidMass); - if (VectorUtils.GeoDistance(waypoints[waypoints.Count - 1], bypassTargetPos, vessel.mainBody) < 200) - waypoints.RemoveAt(waypoints.Count - 1); - if (waypoints.Count > 0) - intermediatePositionGeo = waypoints[0]; + if (VectorUtils.GeoDistance(pathingWaypoints[pathingWaypoints.Count - 1], bypassTargetPos, vessel.mainBody) < 200) + pathingWaypoints.RemoveAt(pathingWaypoints.Count - 1); + if (pathingWaypoints.Count > 0) + intermediatePositionGeo = pathingWaypoints[0]; else bypassTarget = null; } @@ -707,22 +800,22 @@ void checkBypass(Vessel target) private void Pathfind(Vector3 destination) { - waypoints = pathingMatrix.Pathfind( + pathingWaypoints = pathingMatrix.Pathfind( VectorUtils.WorldPositionToGeoCoords(vessel.CoM, vessel.mainBody), destination, vessel.mainBody, SurfaceType, MaxSlopeAngle, AvoidMass); - intermediatePositionGeo = waypoints[0]; + intermediatePositionGeo = pathingWaypoints[0]; } void cycleWaypoint() { - if (waypoints.Count > 1) + if (pathingWaypoints.Count > 1) { - waypoints.RemoveAt(0); - intermediatePositionGeo = waypoints[0]; + pathingWaypoints.RemoveAt(0); + intermediatePositionGeo = pathingWaypoints[0]; } else if (bypassTarget != null) { - waypoints.Clear(); + pathingWaypoints.Clear(); bypassTarget = null; leftPath = true; } diff --git a/BDArmory/Modules/BDModuleVTOLAI.cs b/BDArmory/Control/BDModuleVTOLAI.cs similarity index 67% rename from BDArmory/Modules/BDModuleVTOLAI.cs rename to BDArmory/Control/BDModuleVTOLAI.cs index 9aa390381..d3efa35e3 100644 --- a/BDArmory/Modules/BDModuleVTOLAI.cs +++ b/BDArmory/Control/BDModuleVTOLAI.cs @@ -3,15 +3,17 @@ using System.Collections.Generic; using System.Linq; using System.Text; -using BDArmory.Control; -using BDArmory.Core; -using BDArmory.Core.Extension; -using BDArmory.Core.Utils; -using BDArmory.Misc; -using BDArmory.UI; using UnityEngine; -namespace BDArmory.Modules +using BDArmory.Extensions; +using BDArmory.Guidances; +using BDArmory.Settings; +using BDArmory.UI; +using BDArmory.Utils; +using BDArmory.Weapons; +using BDArmory.Weapons.Missiles; + +namespace BDArmory.Control { public class BDModuleVTOLAI : BDGenericAIBase, IBDAIControl { @@ -38,7 +40,7 @@ public class BDModuleVTOLAI : BDGenericAIBase, IBDAIControl Vector3 upDir; AIUtils.TraversabilityMatrix pathingMatrix; - List waypoints = new List(); + List pathingWaypoints = new List(); bool leftPath = false; protected override Vector3d assignedPositionGeo @@ -240,12 +242,12 @@ public void SetChooseOptions() { UI_ChooseOption broadisdeEditor = (UI_ChooseOption)Fields["OrbitDirectionName"].uiControlEditor; UI_ChooseOption broadisdeFlight = (UI_ChooseOption)Fields["OrbitDirectionName"].uiControlFlight; - UI_ChooseOption SurfaceEditor = (UI_ChooseOption)Fields["SurfaceTypeName"].uiControlEditor; - UI_ChooseOption SurfaceFlight = (UI_ChooseOption)Fields["SurfaceTypeName"].uiControlFlight; broadisdeEditor.onFieldChanged = ChooseOptionsUpdated; broadisdeFlight.onFieldChanged = ChooseOptionsUpdated; - SurfaceEditor.onFieldChanged = ChooseOptionsUpdated; - SurfaceFlight.onFieldChanged = ChooseOptionsUpdated; + //UI_ChooseOption SurfaceEditor = (UI_ChooseOption)Fields["SurfaceTypeName"].uiControlEditor; // If SurfaceTypeName is ever switched from hard-coded to Amphibious, change this + //UI_ChooseOption SurfaceFlight = (UI_ChooseOption)Fields["SurfaceTypeName"].uiControlFlight; + //SurfaceEditor.onFieldChanged = ChooseOptionsUpdated; + //SurfaceFlight.onFieldChanged = ChooseOptionsUpdated; } public void ChooseOptionsUpdated(BaseField field, object obj) @@ -299,28 +301,28 @@ protected override void OnGUI() if (!pilotEnabled || !vessel.isActiveVessel) return; - if (!BDArmorySettings.DRAW_DEBUG_LINES) return; + if (!BDArmorySettings.DEBUG_LINES) return; if (command == PilotCommands.Follow) { - BDGUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position, assignedPositionWorld, 2, Color.red); + GUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position, assignedPositionWorld, 2, Color.red); } - //BDGUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position, vesselTransform.position + targetDirection * 10f, 2, Color.blue); - //BDGUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position + (0.05f * vesselTransform.right), vesselTransform.position + (0.05f * vesselTransform.right), 2, Color.green); + //GUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position, vesselTransform.position + targetDirection * 10f, 2, Color.blue); + //GUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position + (0.05f * vesselTransform.right), vesselTransform.position + (0.05f * vesselTransform.right), 2, Color.green); // Vel vectors - BDGUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position, vesselTransform.position + Vector3.Project(vessel.Velocity(), Vector3.ProjectOnPlane(vesselTransform.up, upDir)).normalized * 10f, 2, Color.cyan); //forward/rev - BDGUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position, vesselTransform.position + Vector3.Project(vessel.Velocity(), Vector3.ProjectOnPlane(vesselTransform.right, upDir)).normalized * 10f, 3, Color.yellow); //lateral + GUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position, vesselTransform.position + Vector3.Project(vessel.Velocity(), vesselTransform.up.ProjectOnPlanePreNormalized(upDir)).normalized * 10f, 2, Color.cyan); //forward/rev + GUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position, vesselTransform.position + Vector3.Project(vessel.Velocity(), vesselTransform.right.ProjectOnPlanePreNormalized(upDir)).normalized * 10f, 3, Color.yellow); //lateral - BDGUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position, vesselTransform.position + targetDirection * 10f, 5, Color.red); - BDGUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position, vesselTransform.position + vesselTransform.up * 1000, 3, Color.white); - BDGUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position, vesselTransform.position + -vesselTransform.forward * 100, 3, Color.yellow); - BDGUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position, vesselTransform.position + vessel.Velocity().normalized * 100, 3, Color.magenta); + GUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position, vesselTransform.position + targetDirection * 10f, 5, Color.red); + GUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position, vesselTransform.position + vesselTransform.up * 1000, 3, Color.white); + GUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position, vesselTransform.position + -vesselTransform.forward * 100, 3, Color.yellow); + GUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position, vesselTransform.position + vessel.Velocity().normalized * 100, 3, Color.magenta); - BDGUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position, vesselTransform.position + rollTarget, 2, Color.blue); + GUIUtils.DrawLineBetweenWorldPositions(vesselTransform.position, vesselTransform.position + rollTarget, 2, Color.blue); - pathingMatrix.DrawDebug(vessel.CoM, waypoints); + pathingMatrix.DrawDebug(vessel.CoM, pathingWaypoints); } #endregion events @@ -338,7 +340,7 @@ protected override void AutoPilot(FlightCtrlState s) targetAltitude = defaultAltitude; aimingMode = false; upDir = VectorUtils.GetUpDirection(vesselTransform.position); - DebugLine(""); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) DebugLine(""); if (initialTakeOff) { @@ -354,198 +356,6 @@ protected override void AutoPilot(FlightCtrlState s) AdjustThrottle(targetVelocity); // set throttle according to our targets and movement } - void PilotLogic_Depreciated() // Surface AI-based with byass target still enabled - { - // check for belowMinAlt - belowMinAltitude = (float)vessel.radarAltitude < minAltitude; - - // check for collisions, but not every frame - if (collisionDetectionTicker == 0) - { - collisionDetectionTicker = 20; - float predictMult = Mathf.Clamp(10 / MaxDrift, 1, 10); - - dodgeVector = null; - - using (var vs = BDATargetManager.LoadedVessels.GetEnumerator()) - while (vs.MoveNext()) - { - if (vs.Current == null || vs.Current == vessel || vs.Current.GetTotalMass() < AvoidMass) continue; - if (!VesselModuleRegistry.ignoredVesselTypes.Contains(vs.Current.vesselType)) - { - var ibdaiControl = VesselModuleRegistry.GetModule(vs.Current); - if (!vs.Current.LandedOrSplashed || (ibdaiControl != null && ibdaiControl.commandLeader != null && ibdaiControl.commandLeader.vessel == vessel)) - continue; - } - dodgeVector = PredictCollisionWithVessel(vs.Current, 5f * predictMult, 0.5f); - if (dodgeVector != null) break; - } - } - else - collisionDetectionTicker--; - - // avoid collisions if any are found - if (dodgeVector != null) - { - targetVelocity = PoweredSteering ? MaxSpeed : CombatSpeed; - targetDirection = (Vector3)dodgeVector; - SetStatus($"Avoiding Collision"); - leftPath = true; - return; - } - - // if bypass target is no longer relevant, remove it - if (bypassTarget != null && ((bypassTarget != targetVessel && bypassTarget != (commandLeader != null ? commandLeader.vessel : null)) - || (VectorUtils.GetWorldSurfacePostion(bypassTargetPos, vessel.mainBody) - bypassTarget.CoM).sqrMagnitude > 500000)) - { - bypassTarget = null; - } - - if (bypassTarget == null) - { - // check for enemy targets and engage - // not checking for guard mode, because if guard mode is off now you can select a target manually and if it is of opposing team, the AI will try to engage while you can man the turrets - if (weaponManager && targetVessel != null && !BDArmorySettings.PEACE_MODE) - { - leftPath = true; - if (collisionDetectionTicker == 5) - checkBypass(targetVessel); - - Vector3 vecToTarget = targetVessel.CoM - vessel.CoM; - float distance = vecToTarget.magnitude; - // lead the target a bit, where 1km/s is a ballpark estimate of the average bullet velocity - float shotSpeed = 1000f; - if ((weaponManager != null ? weaponManager.selectedWeapon : null) is ModuleWeapon wep) - shotSpeed = wep.bulletVelocity; - vecToTarget = targetVessel.PredictPosition(distance / shotSpeed) - vessel.CoM; - - if (BroadsideAttack) - { - Vector3 sideVector = Vector3.Cross(vecToTarget, upDir); //find a vector perpendicular to direction to target - if (collisionDetectionTicker == 10 - && !pathingMatrix.TraversableStraightLine( - VectorUtils.WorldPositionToGeoCoords(vessel.CoM, vessel.mainBody), - VectorUtils.WorldPositionToGeoCoords(vessel.PredictPosition(10), vessel.mainBody), - vessel.mainBody, SurfaceType, MaxPitchAngle, AvoidMass)) - sideSlipDirection = -Math.Sign(Vector3.Dot(vesselTransform.up, sideVector)); // switch sides if we're running ashore - sideVector *= sideSlipDirection; - - float sidestep = distance >= MaxEngagementRange ? Mathf.Clamp01((MaxEngagementRange - distance) / (CombatSpeed * Mathf.Clamp(90 / MaxDrift, 0, 10)) + 1) * AttackAngleAtMaxRange / 90 : // direct to target to attackAngle degrees if over maxrange - (distance <= MinEngagementRange ? 1.5f - distance / (MinEngagementRange * 2) : // 90 to 135 degrees if closer than minrange - (MaxEngagementRange - distance) / (MaxEngagementRange - MinEngagementRange) * (1 - AttackAngleAtMaxRange / 90) + AttackAngleAtMaxRange / 90); // attackAngle to 90 degrees from maxrange to minrange - targetDirection = Vector3.LerpUnclamped(vecToTarget.normalized, sideVector.normalized, sidestep); // interpolate between the side vector and target direction vector based on sidestep - targetVelocity = MaxSpeed; - targetAltitude = CombatAltitude; - DebugLine($"Broadside attack angle {sidestep}"); - } - else // just point at target and go - { - targetAltitude = CombatAltitude; - if ((targetVessel.horizontalSrfSpeed < 10 || Vector3.Dot(Vector3.ProjectOnPlane(targetVessel.srf_vel_direction, upDir), vessel.up) < 0) //if target is stationary or we're facing in opposite directions - && (distance < MinEngagementRange || (distance < (MinEngagementRange * 3 + MaxEngagementRange) / 4 //and too close together - && extendingTarget != null && targetVessel != null && extendingTarget == targetVessel))) - { - extendingTarget = targetVessel; - // not sure if this part is very smart, potential for improvement - targetDirection = -vecToTarget; //extend - targetVelocity = MaxSpeed; - targetAltitude = CombatAltitude; - SetStatus($"Extending"); - return; - } - else - { - extendingTarget = null; - targetDirection = Vector3.ProjectOnPlane(vecToTarget, upDir); - if (Vector3.Dot(targetDirection, vesselTransform.up) < 0) - targetVelocity = PoweredSteering ? MaxSpeed : 0; // if facing away from target - else if (distance >= MaxEngagementRange || distance <= MinEngagementRange) - targetVelocity = MaxSpeed; - else - { - targetVelocity = CombatSpeed / 10 + (MaxSpeed - CombatSpeed / 10) * (distance - MinEngagementRange) / (MaxEngagementRange - MinEngagementRange); //slow down if inside engagement range to extend shooting opportunities - if (weaponManager != null && weaponManager.selectedWeapon != null) - { - switch (weaponManager.selectedWeapon.GetWeaponClass()) - { - case WeaponClasses.Gun: - case WeaponClasses.Rocket: - case WeaponClasses.DefenseLaser: - var gun = (ModuleWeapon)weaponManager.selectedWeapon; - if ((gun.yawRange == 0 || gun.maxPitch == gun.minPitch) && gun.FiringSolutionVector != null) - { - aimingMode = true; - if (Vector3.Angle((Vector3)gun.FiringSolutionVector, vessel.transform.up) < 20) - targetDirection = (Vector3)gun.FiringSolutionVector; - } - break; - } - } - } - targetVelocity = Mathf.Clamp(targetVelocity, PoweredSteering ? CombatSpeed / 5 : 0, MaxSpeed); // maintain a bit of speed if using powered steering - } - } - SetStatus($"Engaging target"); - return; - } - - // follow - if (command == PilotCommands.Follow) - { - leftPath = true; - if (collisionDetectionTicker == 5) - checkBypass(commandLeader.vessel); - - Vector3 targetPosition = GetFormationPosition(); - Vector3 targetDistance = targetPosition - vesselTransform.position; - if (Vector3.Dot(targetDistance, vesselTransform.up) < 0 - && Vector3.ProjectOnPlane(targetDistance, upDir).sqrMagnitude < 250f * 250f - && Vector3.Angle(vesselTransform.up, commandLeader.vessel.srf_velocity) < 0.8f) - { - targetDirection = Vector3.RotateTowards(Vector3.ProjectOnPlane(commandLeader.vessel.srf_vel_direction, upDir), targetDistance, 0.2f, 0); - } - else - { - targetDirection = Vector3.ProjectOnPlane(targetDistance, upDir); - } - targetVelocity = (float)(commandLeader.vessel.horizontalSrfSpeed + (vesselTransform.position - targetPosition).magnitude / 15); - if (Vector3.Dot(targetDirection, vesselTransform.up) < 0 && !PoweredSteering) targetVelocity = 0; - SetStatus($"Following"); - return; - } - } - - // goto - if (leftPath && bypassTarget == null) - { - Pathfind(finalPositionGeo); - leftPath = false; - } - - const float targetRadius = 250f; - targetDirection = Vector3.ProjectOnPlane(assignedPositionWorld - vesselTransform.position, upDir); - - if (targetDirection.sqrMagnitude > targetRadius * targetRadius) - { - if (bypassTarget != null) - targetVelocity = MaxSpeed; - else if (waypoints.Count > 1) - targetVelocity = command == PilotCommands.Attack ? MaxSpeed : CombatSpeed; - else - targetVelocity = Mathf.Clamp((targetDirection.magnitude - targetRadius / 2) / 5f, - 0, command == PilotCommands.Attack ? MaxSpeed : CombatSpeed); - - if (Vector3.Dot(targetDirection, vesselTransform.up) < 0 && !PoweredSteering) targetVelocity = 0; - SetStatus(bypassTarget ? "Repositioning" : "Moving"); - return; - } - - cycleWaypoint(); - - SetStatus($"Not doing anything in particular"); - targetDirection = vesselTransform.up; - } - void PilotLogic() // Surface AI-based with byass target disabled { // check for belowMinAlt @@ -617,12 +427,12 @@ void PilotLogic() // Surface AI-based with byass target disabled targetDirection = Vector3.LerpUnclamped(vecToTarget.normalized, sideVector.normalized, sidestep); // interpolate between the side vector and target direction vector based on sidestep targetVelocity = MaxSpeed; targetAltitude = CombatAltitude; - DebugLine($"Broadside attack angle {sidestep}"); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) DebugLine($"Broadside attack angle {sidestep}"); } else // just point at target and go { targetAltitude = CombatAltitude; - if ((targetVessel.horizontalSrfSpeed < 10 || Vector3.Dot(Vector3.ProjectOnPlane(targetVessel.srf_vel_direction, upDir), vessel.up) < 0) //if target is stationary or we're facing in opposite directions + if ((targetVessel.horizontalSrfSpeed < 10 || Vector3.Dot(targetVessel.srf_vel_direction.ProjectOnPlanePreNormalized(upDir), vessel.up) < 0) //if target is stationary or we're facing in opposite directions && (distance < MinEngagementRange || (distance < (MinEngagementRange * 3 + MaxEngagementRange) / 4 //and too close together && extendingTarget != null && targetVessel != null && extendingTarget == targetVessel))) { @@ -637,7 +447,7 @@ void PilotLogic() // Surface AI-based with byass target disabled else { extendingTarget = null; - targetDirection = Vector3.ProjectOnPlane(vecToTarget, upDir); + targetDirection = vecToTarget.ProjectOnPlanePreNormalized(upDir); if (Vector3.Dot(targetDirection, vesselTransform.up) < 0) targetVelocity = PoweredSteering ? MaxSpeed : 0; // if facing away from target else if (distance >= MaxEngagementRange || distance <= MinEngagementRange) @@ -649,6 +459,23 @@ void PilotLogic() // Surface AI-based with byass target disabled { switch (weaponManager.selectedWeapon.GetWeaponClass()) { + case WeaponClasses.Missile: + MissileBase missile = weaponManager.CurrentMissile; + if (missile.TargetingMode == MissileBase.TargetingModes.Heat && !weaponManager.heatTarget.exists) + { + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) DebugLine($"Attempting heat lock"); + aimingMode = true; + targetDirection = MissileGuidance.GetAirToAirFireSolution(missile, targetVessel); + } + else + { + if (!weaponManager.GetLaunchAuthorization(targetVessel, weaponManager) && (Vector3.SqrMagnitude(targetVessel.vesselTransform.position - vesselTransform.position) < (missile.engageRangeMax * missile.engageRangeMax))) + { + aimingMode = true; + targetDirection = MissileGuidance.GetAirToAirFireSolution(missile, targetVessel); + } + } + break; case WeaponClasses.Gun: case WeaponClasses.Rocket: case WeaponClasses.DefenseLaser: @@ -656,7 +483,7 @@ void PilotLogic() // Surface AI-based with byass target disabled if ((gun.yawRange == 0 || gun.maxPitch == gun.minPitch) && gun.FiringSolutionVector != null) { aimingMode = true; - if (Vector3.Angle(vesselTransform.up, Vector3.ProjectOnPlane((Vector3)gun.FiringSolutionVector, vesselTransform.right)) < MaxPitchAngle) + if (Vector3.Angle(vesselTransform.up, ((Vector3)gun.FiringSolutionVector).ProjectOnPlanePreNormalized(vesselTransform.right)) < MaxPitchAngle) targetDirection = (Vector3)gun.FiringSolutionVector; } break; @@ -680,21 +507,21 @@ void PilotLogic() // Surface AI-based with byass target disabled Vector3 targetPosition = GetFormationPosition(); Vector3 targetDistance = targetPosition - vesselTransform.position; if (Vector3.Dot(targetDistance, vesselTransform.up) < 0 - && Vector3.ProjectOnPlane(targetDistance, upDir).sqrMagnitude < 250f * 250f + && targetDistance.ProjectOnPlanePreNormalized(upDir).sqrMagnitude < 250f * 250f && Vector3.Angle(vesselTransform.up, commandLeader.vessel.srf_velocity) < 0.8f) { - targetDirection = Vector3.RotateTowards(Vector3.ProjectOnPlane(commandLeader.vessel.srf_vel_direction, upDir), targetDistance, 0.2f, 0); + targetDirection = Vector3.RotateTowards(commandLeader.vessel.srf_vel_direction.ProjectOnPlanePreNormalized(upDir), targetDistance, 0.2f, 0); } else { - targetDirection = Vector3.ProjectOnPlane(targetDistance, upDir); + targetDirection = targetDistance.ProjectOnPlanePreNormalized(upDir); } targetVelocity = (float)(commandLeader.vessel.horizontalSrfSpeed + (vesselTransform.position - targetPosition).magnitude / 15); if (Vector3.Dot(targetDirection, vesselTransform.up) < 0 && !PoweredSteering) targetVelocity = 0; SetStatus($"Following"); return; } - + // goto if (leftPath) @@ -704,7 +531,7 @@ void PilotLogic() // Surface AI-based with byass target disabled } const float targetRadius = 250f; - targetDirection = Vector3.ProjectOnPlane(assignedPositionWorld - vesselTransform.position, upDir); + targetDirection = (assignedPositionWorld - vesselTransform.position).ProjectOnPlanePreNormalized(upDir); if (targetDirection.sqrMagnitude > targetRadius * targetRadius) { @@ -747,7 +574,7 @@ void Tactical() { weaveAdjustment = 0; } - DebugLine($"underFire {weaponManager.underFire}, aimingMode {aimingMode}, weaveAdjustment {weaveAdjustment}"); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) DebugLine($"underFire {weaponManager.underFire}, aimingMode {aimingMode}, weaveAdjustment {weaveAdjustment}"); } void AdjustThrottle(float targetSpeed) @@ -763,47 +590,50 @@ void AdjustThrottle(float targetSpeed) void AttitudeControl(FlightCtrlState s) { - Vector3 yawTarget = Vector3.ProjectOnPlane(targetDirection, vesselTransform.forward); + Vector3 yawTarget = targetDirection.ProjectOnPlanePreNormalized(vesselTransform.forward); float yawError = VectorUtils.SignedAngle(vesselTransform.up, yawTarget, vesselTransform.right) + (aimingMode ? 0 : weaveAdjustment); - DebugLine($"yaw target: {yawTarget}, yaw error: {yawError}"); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) DebugLine($"yaw target: {yawTarget}, yaw error: {yawError}"); - float forwardVel = Vector3.Dot(vessel.Velocity(), Vector3.ProjectOnPlane(vesselTransform.up,upDir).normalized); - float forwardAccel = Vector3.Dot(vessel.acceleration_immediate, Vector3.ProjectOnPlane(vesselTransform.up, upDir).normalized); + float forwardVel = Vector3.Dot(vessel.Velocity(), vesselTransform.up.ProjectOnPlanePreNormalized(upDir).normalized); + float forwardAccel = Vector3.Dot(vessel.acceleration_immediate, vesselTransform.up.ProjectOnPlanePreNormalized(upDir).normalized); float velError = targetVelocity - forwardVel; float pitchAngle = Mathf.Clamp(0.015f * -steerMult * velError - 0.33f * -steerDamping * forwardAccel, -MaxPitchAngle, MaxPitchAngle); //Adjust pitchAngle for desired speed if (aimingMode) - pitchAngle = VectorUtils.SignedAngle(vesselTransform.up, Vector3.ProjectOnPlane(targetDirection, vesselTransform.right), -vesselTransform.forward); + pitchAngle = VectorUtils.SignedAngle(vesselTransform.up, targetDirection.ProjectOnPlanePreNormalized(vesselTransform.right), -vesselTransform.forward); else if (belowMinAltitude || targetVelocity == 0f) pitchAngle = 0f; else if (avoidingTerrain) - pitchAngle = 90 - Vector3.Angle(Vector3.ProjectOnPlane(targetDirection, vesselTransform.right), upDir); + pitchAngle = 90 - Vector3.Angle(targetDirection.ProjectOnPlanePreNormalized(vesselTransform.right), upDir); float pitch = 90 - Vector3.Angle(vesselTransform.up, upDir); float pitchError = pitchAngle - pitch; - DebugLine($"target vel: {targetVelocity}, forward vel: {forwardVel}, vel error: {velError}, target pitch: {pitchAngle}, pitch: {pitch}, pitch error: {pitchError}"); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) DebugLine($"target vel: {targetVelocity}, forward vel: {forwardVel}, vel error: {velError}, target pitch: {pitchAngle}, pitch: {pitch}, pitch error: {pitchError}"); float bank = VectorUtils.SignedAngle(-vesselTransform.forward, upDir, -vesselTransform.right); - float latVel = Vector3.Dot(vessel.Velocity(), Vector3.ProjectOnPlane(vesselTransform.right, upDir).normalized); - float latAccel = Vector3.Dot(vessel.acceleration_immediate, Vector3.ProjectOnPlane(vesselTransform.right, upDir).normalized); + float latVel = Vector3.Dot(vessel.Velocity(), vesselTransform.right.ProjectOnPlanePreNormalized(upDir).normalized); + float latAccel = Vector3.Dot(vessel.acceleration_immediate, vesselTransform.right.ProjectOnPlanePreNormalized(upDir).normalized); float latError = targetLatVelocity - latVel; float targetRoll = Mathf.Clamp(0.015f * steerMult * latError - 0.1f * steerDamping * latAccel, -MaxBankAngle, MaxBankAngle); //Adjust pitchAngle for desired speed if (belowMinAltitude || initialTakeOff) { if (avoidingTerrain) + { + terrainAlertNormal = upDir; // FIXME Terrain avoidance isn't implemented for this AI yet. rollTarget = terrainAlertNormal * 100; + } else rollTarget = upDir * 100; targetRoll = VectorUtils.SignedAngle(rollTarget, upDir, -vesselTransform.right); } else - rollTarget = Vector3.RotateTowards(upDir, -vesselTransform.right, targetRoll * Mathf.PI/180f, 0f); + rollTarget = Vector3.RotateTowards(upDir, -vesselTransform.right, targetRoll * Mathf.PI / 180f, 0f); float rollError = targetRoll - bank; - DebugLine($"target lat vel: {targetLatVelocity}, lateral vel: {latVel}, lat vel error: {latError}, target roll: {targetRoll}, bank: {bank}, roll error: {rollError}"); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) DebugLine($"target lat vel: {targetLatVelocity}, lateral vel: {latVel}, lat vel error: {latError}, target roll: {targetRoll}, bank: {bank}, roll error: {rollError}"); Vector3 localAngVel = vessel.angularVelocity; #region PID calculations @@ -817,15 +647,17 @@ void AttitudeControl(FlightCtrlState s) float rollDamping = 0.1f * steerDamping * -localAngVel.y; // For the integral, we track the vector of the pitch and yaw in the 2D plane of the vessel's forward pointing vector so that the pitch and yaw components translate between the axes when the vessel rolls. - directionIntegral = Vector3.ProjectOnPlane(directionIntegral + (pitchError * -vesselTransform.forward + yawError * vesselTransform.right) * Time.deltaTime, vesselTransform.up); + directionIntegral = (directionIntegral + (pitchError * -vesselTransform.forward + yawError * vesselTransform.right) * Time.deltaTime).ProjectOnPlanePreNormalized(vesselTransform.up); if (directionIntegral.sqrMagnitude > 1f) directionIntegral = directionIntegral.normalized; pitchIntegral = steerKiAdjust * Vector3.Dot(directionIntegral, -vesselTransform.forward); yawIntegral = 0.33f * steerKiAdjust * Vector3.Dot(directionIntegral, vesselTransform.right); rollIntegral = 0.1f * steerKiAdjust * Mathf.Clamp(rollIntegral + rollError * Time.deltaTime, -1f, 1f); - s.pitch = pitchProportional + pitchIntegral - pitchDamping; - s.yaw = yawProportional + yawIntegral - yawDamping; - s.roll = rollProportional + rollIntegral - rollDamping; + SetFlightControlState(s, + s.pitch = pitchProportional + pitchIntegral - pitchDamping, + s.yaw = yawProportional + yawIntegral - yawDamping, + s.roll = rollProportional + rollIntegral - rollDamping + ); #endregion if (ManeuverRCS && (Mathf.Abs(s.roll) >= 1 || Mathf.Abs(s.pitch) >= 1 || Mathf.Abs(s.yaw) >= 1)) @@ -862,7 +694,7 @@ void CheckLandingGear() { if (!vessel.LandedOrSplashed) { - if (!belowMinAltitude) + if (vessel.radarAltitude > Mathf.Min(50f, minAltitude / 2f)) vessel.ActionGroups.SetGroup(KSPActionGroup.Gear, false); else vessel.ActionGroups.SetGroup(KSPActionGroup.Gear, true); @@ -915,14 +747,14 @@ void checkBypass(Vessel target) { bypassTarget = target; bypassTargetPos = VectorUtils.WorldPositionToGeoCoords(target.CoM, vessel.mainBody); - waypoints = pathingMatrix.Pathfind( + pathingWaypoints = pathingMatrix.Pathfind( VectorUtils.WorldPositionToGeoCoords(vessel.CoM, vessel.mainBody), VectorUtils.WorldPositionToGeoCoords(target.CoM, vessel.mainBody), vessel.mainBody, SurfaceType, MaxPitchAngle, AvoidMass); - if (VectorUtils.GeoDistance(waypoints[waypoints.Count - 1], bypassTargetPos, vessel.mainBody) < 200) - waypoints.RemoveAt(waypoints.Count - 1); - if (waypoints.Count > 0) - intermediatePositionGeo = waypoints[0]; + if (VectorUtils.GeoDistance(pathingWaypoints[pathingWaypoints.Count - 1], bypassTargetPos, vessel.mainBody) < 200) + pathingWaypoints.RemoveAt(pathingWaypoints.Count - 1); + if (pathingWaypoints.Count > 0) + intermediatePositionGeo = pathingWaypoints[0]; else bypassTarget = null; } @@ -930,22 +762,22 @@ void checkBypass(Vessel target) private void Pathfind(Vector3 destination) { - waypoints = pathingMatrix.Pathfind( + pathingWaypoints = pathingMatrix.Pathfind( VectorUtils.WorldPositionToGeoCoords(vessel.CoM, vessel.mainBody), destination, vessel.mainBody, SurfaceType, MaxPitchAngle, AvoidMass); - intermediatePositionGeo = waypoints[0]; + intermediatePositionGeo = pathingWaypoints[0]; } void cycleWaypoint() { - if (waypoints.Count > 1) + if (pathingWaypoints.Count > 1) { - waypoints.RemoveAt(0); - intermediatePositionGeo = waypoints[0]; + pathingWaypoints.RemoveAt(0); + intermediatePositionGeo = pathingWaypoints[0]; } else if (bypassTarget != null) { - waypoints.Clear(); + pathingWaypoints.Clear(); bypassTarget = null; leftPath = true; } diff --git a/BDArmory/Control/IBDAIControl.cs b/BDArmory/Control/IBDAIControl.cs index 95ab9cefd..02a576bb5 100644 --- a/BDArmory/Control/IBDAIControl.cs +++ b/BDArmory/Control/IBDAIControl.cs @@ -46,7 +46,7 @@ public interface IBDAIControl string currentStatus { get; } - void ReleaseCommand(); + void ReleaseCommand(bool resetAssignedPosition = true, bool storeCommand = true); void CommandFollow(ModuleWingCommander leader, int followerIndex); diff --git a/BDArmory/Modules/MissileFire.cs b/BDArmory/Control/MissileFire.cs similarity index 64% rename from BDArmory/Modules/MissileFire.cs rename to BDArmory/Control/MissileFire.cs index a04e15ba3..76f321a1d 100644 --- a/BDArmory/Modules/MissileFire.cs +++ b/BDArmory/Control/MissileFire.cs @@ -1,24 +1,26 @@ -using System; -using System.Collections; using System.Collections.Generic; +using System.Collections; using System.Linq; using System.Text; +using System; +using UnityEngine; using KSP.Localization; + using BDArmory.Competition; -using BDArmory.Control; -using BDArmory.Core; -using BDArmory.Core.Extension; -using BDArmory.Core.Utils; using BDArmory.CounterMeasure; +using BDArmory.Extensions; +using BDArmory.GameModes; using BDArmory.Guidances; -using BDArmory.Misc; -using BDArmory.Parts; using BDArmory.Radar; +using BDArmory.Settings; using BDArmory.Targeting; using BDArmory.UI; -using UnityEngine; +using BDArmory.Utils; +using BDArmory.WeaponMounts; +using BDArmory.Weapons.Missiles; +using BDArmory.Weapons; -namespace BDArmory.Modules +namespace BDArmory.Control { public class MissileFire : PartModule { @@ -26,6 +28,7 @@ public class MissileFire : PartModule //weapons private List weaponTypes = new List(); + private Dictionary> weaponRanges = new Dictionary>(); public IBDWeapon[] weaponArray; // extension for feature_engagementenvelope: specific lists by weapon engagement type @@ -43,7 +46,8 @@ public class MissileFire : PartModule Transform cameraTransform; float startTime; - public int missilesAway; + public int firedMissiles; + public Dictionary missilesAway; public float totalHP; public float currentHP; @@ -77,21 +81,44 @@ public float rippleRPM } float triggerTimer; - int rippleGunCount; - int _gunRippleIndex; + Dictionary rippleGunCount = new Dictionary(); + Dictionary gunRippleIndex = new Dictionary(); public float gunRippleRpm; - public int gunRippleIndex + public void incrementRippleIndex(string weaponname) { - get { return _gunRippleIndex; } - set + if (!gunRippleIndex.ContainsKey(weaponname)) { - _gunRippleIndex = value; - if (_gunRippleIndex >= rippleGunCount) + UpdateList(); + if (!gunRippleIndex.ContainsKey(weaponname)) { - _gunRippleIndex = 0; + Debug.LogError($"[BDArmory.MissileFire]: Weapon {weaponname} on {vessel.vesselName} does not exist in the gunRippleIndex!"); + return; } } + gunRippleIndex[weaponname]++; + if (gunRippleIndex[weaponname] >= GetRippleGunCount(weaponname)) + { + gunRippleIndex[weaponname] = 0; + } + } + + public int GetRippleIndex(string weaponname) + { + if (gunRippleIndex.TryGetValue(weaponname, out int rippleIndex)) + { + return rippleIndex; + } + else return 0; + } + + public int GetRippleGunCount(string weaponname) + { + if (rippleGunCount.TryGetValue(weaponname, out int rippleCount)) + { + return rippleCount; + } + else return 0; } //ripple stuff @@ -162,7 +189,7 @@ void ParseRippleOptions() //Debug.Log("[BDArmory.MissileFire]: Parsing ripple options"); if (!string.IsNullOrEmpty(rippleData)) { - //Debug.Log("[BDArmory.MissileFire]: Ripple data: " + rippleData); + // Debug.Log("[BDArmory.MissileFire]: Ripple data: " + rippleData); try { using (IEnumerator weapon = rippleData.Split(new char[] { ';' }).AsEnumerable().GetEnumerator()) @@ -200,19 +227,21 @@ void SaveRippleOptions(ConfigNode node) while (wpnName.MoveNext()) { if (wpnName.Current == null) continue; - rippleData += $"{wpnName},{rippleDictionary[wpnName.Current].rippleFire},{rippleDictionary[wpnName.Current].rpm};"; + rippleData += $"{wpnName.Current},{rippleDictionary[wpnName.Current].rippleFire},{rippleDictionary[wpnName.Current].rpm};"; } node.SetValue("RippleData", rippleData, true); } //Debug.Log("[BDArmory.MissileFire]: Saved ripple data"); } + public float barrageStagger = 0f; public bool hasSingleFired; public bool engageAir = true; public bool engageMissile = true; public bool engageSrf = true; public bool engageSLW = true; + public bool weaponsListNeedsUpdating = false; public void ToggleEngageAir() { @@ -276,6 +305,7 @@ public void ToggleEngageSLW() } //bomb aimer + bool unguidedWeapon = false; Part bombPart; Vector3 bombAimerPosition = Vector3.zero; Texture2D bombAimerTexture = GameDatabase.Instance.GetTexture("BDArmory/Textures/grayCircle", false); @@ -307,6 +337,7 @@ public void ToggleEngageSLW() float targetScanTimer; Vessel guardTarget; public TargetInfo currentTarget; + public int engagedTargets = 0; public List targetsAssigned; //secondary targets list public List missilesAssigned; //secondary missile targets list TargetInfo overrideTarget; //used for setting target next guard scan for stuff like assisting teammates @@ -327,17 +358,33 @@ public bool TargetOverride //targeting pods public ModuleTargetingCamera mainTGP = null; - public List targetingPods = new List(); + public List targetingPods { get { if (modulesNeedRefreshing) RefreshModules(); return _targetingPods; } } + List _targetingPods = new List(); //radar - public List radars = new List(); + public List radars { get { if (modulesNeedRefreshing) RefreshModules(); return _radars; } } + public List _radars = new List(); + public int MaxradarLocks = 0; public VesselRadarData vesselRadarData; + public bool _radarsEnabled = false; + + public List irsts { get { if (modulesNeedRefreshing) RefreshModules(); return _irsts; } } + public List _irsts = new List(); //jammers - public List jammers = new List(); + public List jammers { get { if (modulesNeedRefreshing) RefreshModules(); return _jammers; } } + public List _jammers = new List(); + + //cloak generators + public List cloaks { get { if (modulesNeedRefreshing) RefreshModules(); return _cloaks; } } + public List _cloaks = new List(); //other modules - public List wmModules = new List(); + public List wmModules { get { if (modulesNeedRefreshing) RefreshModules(); return _wmModules; } } + List _wmModules = new List(); + + bool modulesNeedRefreshing = true; // Refresh modules as needed — avoids excessive calling due to events. + bool cmPrioritiesNeedRefreshing = true; // Refresh CM priorities as needed. //wingcommander public ModuleWingCommander wingCommander; @@ -362,6 +409,14 @@ public RadarWarningReceiver rwr public GPSTargetInfo designatedGPSInfo; public Vector3d designatedGPSCoords => designatedGPSInfo.gpsCoordinates; + public int designatedGPSCoordsIndex = -1; + public void SelectNextGPSTarget() + { + var targets = BDATargetManager.GPSTargetList(Team); + if (targets.Count == 0) return; + if (++designatedGPSCoordsIndex >= targets.Count) designatedGPSCoordsIndex = 0; + designatedGPSInfo = targets[designatedGPSCoordsIndex]; + } //weapon slaving public bool slavingTurrets = false; @@ -372,6 +427,7 @@ public RadarWarningReceiver rwr //current weapon ref public MissileBase CurrentMissile; + public MissileBase PreviousMissile; public ModuleWeapon currentGun { @@ -407,6 +463,22 @@ public ModuleWeapon previousGun float underAttackLastNotified = 0f; public bool underFire; float underFireLastNotified = 0f; + HashSet recentlyFiringWeaponClasses = new HashSet { WeaponClasses.Gun, WeaponClasses.Rocket, WeaponClasses.DefenseLaser }; + public bool recentlyFiring // Recently firing property for CameraTools. + { + get + { + if (guardFiringMissile) return true; // Fired a missile recently. + foreach (var weaponCandidate in weaponArray) + { + if (weaponCandidate == null || !recentlyFiringWeaponClasses.Contains(weaponCandidate.GetWeaponClass())) continue; + var weapon = (ModuleWeapon)weaponCandidate; + if (weapon == null) continue; + if (Time.time - weapon.timeFired < BDArmorySettings.CAMERA_SWITCH_FREQUENCY / 2f) return true; // Fired a gun recently. + } + return false; + } + } public Vector3 incomingThreatPosition; public Vessel incomingThreatVessel; @@ -418,6 +490,8 @@ public ModuleWeapon previousGun public bool debilitated = false; public bool guardFiringMissile; + public bool hasAntiRadiationOrdinance; + public float[] antiradTargets; public bool antiRadTargetAcquired; Vector3 antiRadiationTarget; public bool laserPointDetected; @@ -427,43 +501,48 @@ public ModuleWeapon previousGun #region KSPFields,events,actions [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_FiringInterval"),//Firing Interval - UI_FloatRange(minValue = 0.5f, maxValue = 60f, stepIncrement = 0.5f, scene = UI_Scene.All)] + UI_FloatRange(minValue = 0.5f, maxValue = 60f, stepIncrement = 0.5f, scene = UI_Scene.All)] public float targetScanInterval = 1; // extension for feature_engagementenvelope: burst length for guns [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_FiringBurstLength"),//Firing Burst Length - UI_FloatRange(minValue = 0f, maxValue = 10f, stepIncrement = 0.05f, scene = UI_Scene.All)] + UI_FloatRange(minValue = 0f, maxValue = 10f, stepIncrement = 0.05f, scene = UI_Scene.All)] public float fireBurstLength = 1; [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_FiringTolerance"),//Firing Tolerance - UI_FloatRange(minValue = 0f, maxValue = 4f, stepIncrement = 0.05f, scene = UI_Scene.All)] - public float AutoFireCosAngleAdjustment = 1.4f; //tune Autofire angle in WM GUI + UI_FloatRange(minValue = 0f, maxValue = 4f, stepIncrement = 0.05f, scene = UI_Scene.All)] + public float AutoFireCosAngleAdjustment = 1.0f; //tune Autofire angle in WM GUI public float adjustedAutoFireCosAngle = 0.99970f; //increased to 3 deg from 1, max increased to v1.3.8 default of 4 [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_FieldOfView"),//Field of View - UI_FloatRange(minValue = 10f, maxValue = 360f, stepIncrement = 10f, scene = UI_Scene.All)] + UI_FloatRange(minValue = 10f, maxValue = 360f, stepIncrement = 10f, scene = UI_Scene.All)] public float guardAngle = 360; [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = false, guiName = "#LOC_BDArmory_VisualRange"),//Visual Range - UI_FloatRange(minValue = 100f, maxValue = 200000f, stepIncrement = 100f, scene = UI_Scene.All)] + UI_FloatRange(minValue = 100f, maxValue = 200000f, stepIncrement = 100f, scene = UI_Scene.All)] public float guardRange = 200000f; [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = false, guiName = "#LOC_BDArmory_GunsRange"),//Guns Range - UI_FloatRange(minValue = 0f, maxValue = 10000f, stepIncrement = 10f, scene = UI_Scene.All)] + UI_FloatRange(minValue = 0f, maxValue = 10000f, stepIncrement = 10f, scene = UI_Scene.All)] public float gunRange = 2500f; public float maxGunRange = 0f; [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_WMWindow_MultiTargetNum"),//Max Turret Targets - UI_FloatRange(minValue = 1, maxValue = 10, stepIncrement = 1, scene = UI_Scene.All)] + UI_FloatRange(minValue = 1, maxValue = 10, stepIncrement = 1, scene = UI_Scene.All)] public float multiTargetNum = 1; + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_WMWindow_MultiMissileNum"),//Max Missile Targets + UI_FloatRange(minValue = 1, maxValue = 10, stepIncrement = 1, scene = UI_Scene.All)] + public float multiMissileTgtNum = 1; + public const float maxAllowableMissilesOnTarget = 18f; - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_MissilesORTarget"), UI_FloatRange(minValue = 1f, maxValue = maxAllowableMissilesOnTarget, stepIncrement = 1f, scene = UI_Scene.All)]//Missiles/Target + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_MissilesORTarget"),//Missiles/Target + UI_FloatRange(minValue = 1f, maxValue = maxAllowableMissilesOnTarget, stepIncrement = 1f, scene = UI_Scene.All)] public float maxMissilesOnTarget = 1; #region TargetSettings @@ -482,8 +561,11 @@ public float [KSPField(isPersistant = true)] public bool targetMass = false; + [KSPField(isPersistant = true)] + public bool targetRandom = false; + [KSPField(guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_targetSetting")]//Target Setting - public string targetingString = Localizer.Format("#LOC_BDArmory_TargetCOM"); + public string targetingString = StringUtils.Localize("#LOC_BDArmory_TargetCOM"); [KSPEvent(guiActive = true, guiActiveEditor = true, active = true, guiName = "#LOC_BDArmory_Selecttargeting")]//Select Targeting Option public void SelectTargeting() { @@ -503,99 +585,109 @@ public void SelectTargeting() [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_TargetPriority_TargetScore", advancedTweakable = true, groupName = "targetPriority", groupDisplayName = "#LOC_BDArmory_TargetPriority_Settings", groupStartCollapsed = true), UI_Label(scene = UI_Scene.All)] public string TargetScoreLabel = ""; - private string targetBiasLabel = Localizer.Format("#LOC_BDArmory_TargetPriority_CurrentTargetBias"); + private string targetBiasLabel = StringUtils.Localize("#LOC_BDArmory_TargetPriority_CurrentTargetBias"); [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_TargetPriority_CurrentTargetBias", advancedTweakable = true, groupName = "targetPriority", groupDisplayName = "#LOC_BDArmory_TargetPriority_Settings", groupStartCollapsed = true),//Current target bias - UI_FloatRange(minValue = -10f, maxValue = 10f, stepIncrement = 0.1f, scene = UI_Scene.All)] + UI_FloatRange(minValue = -10f, maxValue = 10f, stepIncrement = 0.1f, scene = UI_Scene.All)] public float targetBias = 1.1f; - private string targetRangeLabel = Localizer.Format("#LOC_BDArmory_TargetPriority_TargetProximity"); + private string targetPreferenceLabel = StringUtils.Localize("#LOC_BDArmory_TargetPriority_AirVsGround"); + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_TargetPriority_AirVsGround", advancedTweakable = true, groupName = "targetPriority", groupDisplayName = "#LOC_BDArmory_TargetPriority_Settings", groupStartCollapsed = true),//Target Preference + UI_FloatRange(minValue = -10f, maxValue = 10f, stepIncrement = 0.1f, scene = UI_Scene.All)] + public float targetWeightAirPreference = 0f; + + private string targetRangeLabel = StringUtils.Localize("#LOC_BDArmory_TargetPriority_TargetProximity"); [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_TargetPriority_TargetProximity", advancedTweakable = true, groupName = "targetPriority", groupDisplayName = "#LOC_BDArmory_TargetPriority_Settings", groupStartCollapsed = true),//Target Range - UI_FloatRange(minValue = -10f, maxValue = 10f, stepIncrement = 0.1f, scene = UI_Scene.All)] + UI_FloatRange(minValue = -10f, maxValue = 10f, stepIncrement = 0.1f, scene = UI_Scene.All)] public float targetWeightRange = 1f; - private string targetATALabel = Localizer.Format("#LOC_BDArmory_TargetPriority_CloserAngleToTarget"); + private string targetATALabel = StringUtils.Localize("#LOC_BDArmory_TargetPriority_CloserAngleToTarget"); [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_TargetPriority_CloserAngleToTarget", advancedTweakable = true, groupName = "targetPriority", groupDisplayName = "#LOC_BDArmory_TargetPriority_Settings", groupStartCollapsed = true),//Antenna Train Angle - UI_FloatRange(minValue = -10f, maxValue = 10f, stepIncrement = 0.1f, scene = UI_Scene.All)] + UI_FloatRange(minValue = -10f, maxValue = 10f, stepIncrement = 0.1f, scene = UI_Scene.All)] public float targetWeightATA = 1f; - private string targetAoDLabel = Localizer.Format("#LOC_BDArmory_TargetPriority_AngleOverDistance"); + private string targetAoDLabel = StringUtils.Localize("#LOC_BDArmory_TargetPriority_AngleOverDistance"); [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_TargetPriority_AngleOverDistance", advancedTweakable = true, groupName = "targetPriority", groupDisplayName = "#LOC_BDArmory_TargetPriority_Settings", groupStartCollapsed = true),//Angle/Distance - UI_FloatRange(minValue = -10f, maxValue = 10f, stepIncrement = 0.1f, scene = UI_Scene.All)] + UI_FloatRange(minValue = -10f, maxValue = 10f, stepIncrement = 0.1f, scene = UI_Scene.All)] public float targetWeightAoD = 0f; - private string targetAccelLabel = Localizer.Format("#LOC_BDArmory_TargetPriority_TargetAcceleration"); + private string targetAccelLabel = StringUtils.Localize("#LOC_BDArmory_TargetPriority_TargetAcceleration"); [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_TargetPriority_TargetAcceleration", advancedTweakable = true, groupName = "targetPriority", groupDisplayName = "#LOC_BDArmory_TargetPriority_Settings", groupStartCollapsed = true),//Target Acceleration - UI_FloatRange(minValue = -10f, maxValue = 10f, stepIncrement = 0.1f, scene = UI_Scene.All)] + UI_FloatRange(minValue = -10f, maxValue = 10f, stepIncrement = 0.1f, scene = UI_Scene.All)] public float targetWeightAccel = 0; - private string targetClosureTimeLabel = Localizer.Format("#LOC_BDArmory_TargetPriority_ShorterClosingTime"); + private string targetClosureTimeLabel = StringUtils.Localize("#LOC_BDArmory_TargetPriority_ShorterClosingTime"); [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_TargetPriority_ShorterClosingTime", advancedTweakable = true, groupName = "targetPriority", groupDisplayName = "#LOC_BDArmory_TargetPriority_Settings", groupStartCollapsed = true),//Target Closure Time - UI_FloatRange(minValue = -10f, maxValue = 10f, stepIncrement = 0.1f, scene = UI_Scene.All)] + UI_FloatRange(minValue = -10f, maxValue = 10f, stepIncrement = 0.1f, scene = UI_Scene.All)] public float targetWeightClosureTime = 0f; - private string targetWeaponNumberLabel = Localizer.Format("#LOC_BDArmory_TargetPriority_TargetWeaponNumber"); + private string targetWeaponNumberLabel = StringUtils.Localize("#LOC_BDArmory_TargetPriority_TargetWeaponNumber"); [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_TargetPriority_TargetWeaponNumber", advancedTweakable = true, groupName = "targetPriority", groupDisplayName = "#LOC_BDArmory_TargetPriority_Settings", groupStartCollapsed = true),//Target Weapon Number - UI_FloatRange(minValue = -10f, maxValue = 10f, stepIncrement = 0.1f, scene = UI_Scene.All)] + UI_FloatRange(minValue = -10f, maxValue = 10f, stepIncrement = 0.1f, scene = UI_Scene.All)] public float targetWeightWeaponNumber = 0; - private string targetMassLabel = Localizer.Format("#LOC_BDArmory_TargetPriority_TargetMass"); + private string targetMassLabel = StringUtils.Localize("#LOC_BDArmory_TargetPriority_TargetMass"); [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_TargetPriority_TargetMass", advancedTweakable = true, groupName = "targetPriority", groupDisplayName = "#LOC_BDArmory_TargetPriority_Settings", groupStartCollapsed = true),//Target Mass - UI_FloatRange(minValue = -10f, maxValue = 10f, stepIncrement = 0.1f, scene = UI_Scene.All)] + UI_FloatRange(minValue = -10f, maxValue = 10f, stepIncrement = 0.1f, scene = UI_Scene.All)] public float targetWeightMass = 0; - private string targetFriendliesEngagingLabel = Localizer.Format("#LOC_BDArmory_TargetPriority_FewerTeammatesEngaging"); + private string targetDmgLabel = StringUtils.Localize("#LOC_BDArmory_TargetPriority_TargetDmg"); + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_TargetPriority_TargetDmg", advancedTweakable = true, groupName = "targetPriority", groupDisplayName = "#LOC_BDArmory_TargetPriority_Settings", groupStartCollapsed = true),//Target Damage + UI_FloatRange(minValue = -10f, maxValue = 10f, stepIncrement = 0.1f, scene = UI_Scene.All)] + public float targetWeightDamage = -1; + + private string targetFriendliesEngagingLabel = StringUtils.Localize("#LOC_BDArmory_TargetPriority_FewerTeammatesEngaging"); [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_TargetPriority_FewerTeammatesEngaging", advancedTweakable = true, groupName = "targetPriority", groupDisplayName = "#LOC_BDArmory_TargetPriority_Settings", groupStartCollapsed = true),//Number Friendlies Engaging - UI_FloatRange(minValue = -10f, maxValue = 10f, stepIncrement = 0.1f, scene = UI_Scene.All)] + UI_FloatRange(minValue = -10f, maxValue = 10f, stepIncrement = 0.1f, scene = UI_Scene.All)] public float targetWeightFriendliesEngaging = 1f; - private string targetThreatLabel = Localizer.Format("#LOC_BDArmory_TargetPriority_TargetThreat"); + private string targetThreatLabel = StringUtils.Localize("#LOC_BDArmory_TargetPriority_TargetThreat"); [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_TargetPriority_TargetThreat", advancedTweakable = true, groupName = "targetPriority", groupDisplayName = "#LOC_BDArmory_TargetPriority_Settings", groupStartCollapsed = true),//Target threat - UI_FloatRange(minValue = -10f, maxValue = 10f, stepIncrement = 0.1f, scene = UI_Scene.All)] + UI_FloatRange(minValue = -10f, maxValue = 10f, stepIncrement = 0.1f, scene = UI_Scene.All)] public float targetWeightThreat = 1f; - private string targetProtectTeammateLabel = Localizer.Format("#LOC_BDArmory_TargetPriority_TargetProtectTeammate"); + private string targetProtectTeammateLabel = StringUtils.Localize("#LOC_BDArmory_TargetPriority_TargetProtectTeammate"); [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_TargetPriority_TargetProtectTeammate", advancedTweakable = true, groupName = "targetPriority", groupDisplayName = "#LOC_BDArmory_TargetPriority_Settings", groupStartCollapsed = true),//Protect Teammates - UI_FloatRange(minValue = -10f, maxValue = 10f, stepIncrement = 0.1f, scene = UI_Scene.All)] + UI_FloatRange(minValue = -10f, maxValue = 10f, stepIncrement = 0.1f, scene = UI_Scene.All)] public float targetWeightProtectTeammate = 0f; - private string targetProtectVIPLabel = Localizer.Format("#LOC_BDArmory_TargetPriority_TargetProtectVIP"); + private string targetProtectVIPLabel = StringUtils.Localize("#LOC_BDArmory_TargetPriority_TargetProtectVIP"); [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_TargetPriority_TargetProtectVIP", advancedTweakable = true, groupName = "targetPriority", groupDisplayName = "#LOC_BDArmory_TargetPriority_Settings", groupStartCollapsed = true),//Protect VIPs - UI_FloatRange(minValue = -10f, maxValue = 10f, stepIncrement = 0.1f, scene = UI_Scene.All)] + UI_FloatRange(minValue = -10f, maxValue = 10f, stepIncrement = 0.1f, scene = UI_Scene.All)] public float targetWeightProtectVIP = 0f; - private string targetAttackVIPLabel = Localizer.Format("#LOC_BDArmory_TargetPriority_TargetAttackVIP"); + private string targetAttackVIPLabel = StringUtils.Localize("#LOC_BDArmory_TargetPriority_TargetAttackVIP"); [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_TargetPriority_TargetAttackVIP", advancedTweakable = true, groupName = "targetPriority", groupDisplayName = "#LOC_BDArmory_TargetPriority_Settings", groupStartCollapsed = true),//Attack Enemy VIPs - UI_FloatRange(minValue = -10f, maxValue = 10f, stepIncrement = 0.1f, scene = UI_Scene.All)] + UI_FloatRange(minValue = -10f, maxValue = 10f, stepIncrement = 0.1f, scene = UI_Scene.All)] public float targetWeightAttackVIP = 0f; #endregion #region Countermeasure Settings [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_CMThreshold", advancedTweakable = true, groupName = "cmSettings", groupDisplayName = "#LOC_BDArmory_Countermeasure_Settings", groupStartCollapsed = true),// Countermeasure dispensing time threshold - UI_FloatRange(minValue = 1f, maxValue = 60f, stepIncrement = 0.5f, scene = UI_Scene.All)] + UI_FloatRange(minValue = 1f, maxValue = 60f, stepIncrement = 0.5f, scene = UI_Scene.All)] public float cmThreshold = 5f; // Works well [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_CMRepetition", advancedTweakable = true, groupName = "cmSettings", groupDisplayName = "#LOC_BDArmory_Countermeasure_Settings", groupStartCollapsed = true),// Flare dispensing repetition - UI_FloatRange(minValue = 1f, maxValue = 20f, stepIncrement = 1f, scene = UI_Scene.All)] + UI_FloatRange(minValue = 1f, maxValue = 20f, stepIncrement = 1f, scene = UI_Scene.All)] public float cmRepetition = 3f; // Prior default was 4 [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_CMInterval", advancedTweakable = true, groupName = "cmSettings", groupDisplayName = "#LOC_BDArmory_Countermeasure_Settings", groupStartCollapsed = true),// Flare dispensing interval - UI_FloatRange(minValue = 0.1f, maxValue = 1f, stepIncrement = 0.1f, scene = UI_Scene.All)] + UI_FloatRange(minValue = 0.1f, maxValue = 1f, stepIncrement = 0.1f, scene = UI_Scene.All)] public float cmInterval = 0.2f; // Prior default was 0.6 [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_CMWaitTime", advancedTweakable = true, groupName = "cmSettings", groupDisplayName = "#LOC_BDArmory_Countermeasure_Settings", groupStartCollapsed = true),// Flare dispensing wait time - UI_FloatRange(minValue = 0.1f, maxValue = 10f, stepIncrement = 0.1f, scene = UI_Scene.All)] + UI_FloatRange(minValue = 0.1f, maxValue = 10f, stepIncrement = 0.1f, scene = UI_Scene.All)] public float cmWaitTime = 0.7f; // Works well [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_ChaffRepetition", advancedTweakable = true, groupName = "cmSettings", groupDisplayName = "#LOC_BDArmory_Countermeasure_Settings", groupStartCollapsed = true),// Chaff dispensing repetition - UI_FloatRange(minValue = 1f, maxValue = 20f, stepIncrement = 1f, scene = UI_Scene.All)] + UI_FloatRange(minValue = 1f, maxValue = 20f, stepIncrement = 1f, scene = UI_Scene.All)] public float chaffRepetition = 2f; // Prior default was 4 [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_ChaffInterval", advancedTweakable = true, groupName = "cmSettings", groupDisplayName = "#LOC_BDArmory_Countermeasure_Settings", groupStartCollapsed = true),// Chaff dispensing interval - UI_FloatRange(minValue = 0.1f, maxValue = 1f, stepIncrement = 0.1f, scene = UI_Scene.All)] + UI_FloatRange(minValue = 0.1f, maxValue = 1f, stepIncrement = 0.1f, scene = UI_Scene.All)] public float chaffInterval = 0.5f; // Prior default was 0.6 [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_ChaffWaitTime", advancedTweakable = true, groupName = "cmSettings", groupDisplayName = "#LOC_BDArmory_Countermeasure_Settings", groupStartCollapsed = true),// Chaff dispensing wait time - UI_FloatRange(minValue = 0.1f, maxValue = 10f, stepIncrement = 0.1f, scene = UI_Scene.All)] + UI_FloatRange(minValue = 0.1f, maxValue = 10f, stepIncrement = 0.1f, scene = UI_Scene.All)] public float chaffWaitTime = 0.6f; // Works well #endregion @@ -618,12 +710,7 @@ public void ToggleGuardMode() weapon.Current.visualTargetVessel = null; weapon.Current.visualTargetPart = null; weapon.Current.autoFire = false; - weapon.Current.aiControlled = false; - if (weapon.Current.isAPS) - { - weapon.Current.DisableWeapon(); - weapon.Current.aiControlled = false; - } + if (!weapon.Current.isAPS) weapon.Current.aiControlled = false; } weaponIndex = 0; selectedWeapon = null; @@ -634,12 +721,36 @@ public void ToggleGuardMode() while (weapon.MoveNext()) { if (weapon.Current == null) continue; - if (weapon.Current.isAPS) + weapon.Current.aiControlled = true; + } + if (radars.Count > 0) + { + using (List.Enumerator rd = radars.GetEnumerator()) + while (rd.MoveNext()) { - weapon.Current.EnableWeapon(); - weapon.Current.aiControlled = true; + if (rd.Current != null || rd.Current.canLock) + { + rd.Current.EnableRadar(); + _radarsEnabled = true; + } } - } + } + if (irsts.Count > 0) + { + using (List.Enumerator rd = irsts.GetEnumerator()) + while (rd.MoveNext()) + { + if (rd.Current != null) + { + rd.Current.EnableIRST(); + } + } + } + if (hasAntiRadiationOrdinance) + { + if (rwr && !rwr.rwrEnabled) rwr.EnableRWR(); + if (rwr && rwr.rwrEnabled && !rwr.displayRWR) rwr.displayRWR = true; + } } } @@ -683,7 +794,7 @@ public void AGJettisonWeapon(KSPActionParam param) } } - [KSPAction("Deploy Kerbal's Parachute")] // If there's an EVAing kerbal. + [KSPAction("Deploy Kerbals' Parachutes")] // If there's an EVAing kerbal. public void AGDeployKerbalsParachute(KSPActionParam param) { foreach (var chute in VesselModuleRegistry.GetModules(vessel)) @@ -695,6 +806,34 @@ public void AGDeployKerbalsParachute(KSPActionParam param) } } + [KSPAction("Remove Kerbals' Helmets")] // Note: removing helmets only works for the active vessel, so this waits until the vessel is active before doing so. + public void AGRemoveKerbalsHelmets(KSPActionParam param) + { + if (vessel.isActiveVessel) + { + foreach (var kerbal in VesselModuleRegistry.GetModules(vessel).Where(k => k != null)) kerbal.ToggleHelmetAndNeckRing(false, false); + waitingToRemoveHelmets = false; + } + else if (!waitingToRemoveHelmets) StartCoroutine(RemoveKerbalsHelmetsWhenActiveVessel()); + } + + bool waitingToRemoveHelmets = false; + IEnumerator RemoveKerbalsHelmetsWhenActiveVessel() + { + waitingToRemoveHelmets = true; + yield return new WaitUntil(() => (vessel == null || vessel.isActiveVessel)); + if (vessel == null) yield break; + foreach (var kerbal in VesselModuleRegistry.GetModules(vessel)) + { + if (kerbal == null) continue; + if (kerbal.CanSafelyRemoveHelmet()) + { + kerbal.ToggleHelmetAndNeckRing(false, false); + } + } + waitingToRemoveHelmets = false; + } + [KSPAction("Self-destruct")] // Self-destruct public void AGSelfDestruct(KSPActionParam param) { @@ -705,6 +844,12 @@ public void AGSelfDestruct(KSPActionParam param) PartExploderSystem.AddPartToExplode(part); } } + foreach (var tnt in VesselModuleRegistry.GetModules(vessel)) + { + if (tnt == null) continue; + tnt.ArmAG(null); + tnt.DetonateIfPossible(); + } } public BDTeam Team @@ -827,10 +972,8 @@ public void ToggleArm() if (isArmed) audioSource.PlayOneShot(armOnSound); else audioSource.PlayOneShot(armOffSound); } - [KSPField(isPersistant = false, guiActive = true, guiName = "#LOC_BDArmory_Weapon")]//Weapon - public string selectedWeaponString = - "None"; + public string selectedWeaponString = "None"; IBDWeapon sw; @@ -847,9 +990,23 @@ public IBDWeapon selectedWeapon if (weapon.Current.GetWeaponClass() == WeaponClasses.Gun || weapon.Current.GetWeaponClass() == WeaponClasses.Rocket || weapon.Current.GetWeaponClass() == WeaponClasses.DefenseLaser) { var gun = weapon.Current.GetPart().FindModuleImplementing(); + sw = weapon.Current; //check against salvofiring turrets - if all guns overheat at the same time, turrets get stuck in standby mode if (gun.isReloading || gun.isOverheated || gun.pointingAtSelf || !(gun.ammoCount > 0 || BDArmorySettings.INFINITE_AMMO)) continue; //instead of returning the first weapon in a weapon group, return the first weapon in a group that actually can fire + //no check for if all weapons in the group are reloading/overheated... + //Doc also was floating the idea of a 'use this gun' button for aiming, though that would be more a PilotAi thing... + } + if (weapon.Current.GetWeaponClass() == WeaponClasses.Missile || weapon.Current.GetWeaponClass() == WeaponClasses.Bomb || weapon.Current.GetWeaponClass() == WeaponClasses.SLW) + { + var msl = weapon.Current.GetPart().FindModuleImplementing(); + if (msl == null) continue; + + unguidedWeapon = (weaponArray[weaponIndex].GetWeaponClass() == WeaponClasses.Bomb || (weaponArray[weaponIndex].GetWeaponClass() == WeaponClasses.Missile && + (msl.TargetingMode == MissileBase.TargetingModes.None || msl.GuidanceMode == MissileBase.GuidanceModes.None) || (msl.TargetingMode == MissileBase.TargetingModes.Laser && BDATargetManager.ActiveLasers.Count <= 0) + || (msl.TargetingMode == MissileBase.TargetingModes.Radar && !_radarsEnabled))); + if (msl.launched || msl.HasFired) continue; //return first missile that is ready to fire + if (msl.GetEngageRange() != selectedWeaponsEngageRangeMax) continue; + sw = weapon.Current; } - sw = weapon.Current; break; } return sw; @@ -860,16 +1017,19 @@ public IBDWeapon selectedWeapon previousSelectedWeapon = sw; sw = value; selectedWeaponString = GetWeaponName(value); + selectedWeaponsEngageRangeMax = GetWeaponRange(value); UpdateSelectedWeaponState(); } } IBDWeapon previousSelectedWeapon { get; set; } + public float selectedWeaponsEngageRangeMax { get; private set; } = 0; + [KSPAction("Fire Missile")] public void AGFire(KSPActionParam param) { - FireMissile(); + FireMissileManually(false); } [KSPAction("Fire Guns (Hold)")] @@ -934,10 +1094,13 @@ public void SetAFCAA() public void OnAFCAAUpdated(BaseField field, object obj) { adjustedAutoFireCosAngle = Mathf.Cos((AutoFireCosAngleAdjustment * Mathf.Deg2Rad)); - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.MissileFire]: Setting AFCAA to " + adjustedAutoFireCosAngle); + //if (BDArmorySettings.DEBUG_LABELS) Debug.Log("[BDArmory.MissileFire]: Setting AFCAA to " + adjustedAutoFireCosAngle); } #endregion KSPFields,events,actions + RaycastHit[] clearanceHits = new RaycastHit[10]; + + private LineRenderer lr = null; private StringBuilder debugString = new StringBuilder(); #endregion Declarations @@ -969,11 +1132,11 @@ public override void OnLoad(ConfigNode node) public override void OnAwake() { - clickSound = GameDatabase.Instance.GetAudioClip("BDArmory/Sounds/click"); - warningSound = GameDatabase.Instance.GetAudioClip("BDArmory/Sounds/warning"); - armOnSound = GameDatabase.Instance.GetAudioClip("BDArmory/Sounds/armOn"); - armOffSound = GameDatabase.Instance.GetAudioClip("BDArmory/Sounds/armOff"); - heatGrowlSound = GameDatabase.Instance.GetAudioClip("BDArmory/Sounds/heatGrowl"); + clickSound = SoundUtils.GetAudioClip("BDArmory/Sounds/click"); + warningSound = SoundUtils.GetAudioClip("BDArmory/Sounds/warning"); + armOnSound = SoundUtils.GetAudioClip("BDArmory/Sounds/armOn"); + armOffSound = SoundUtils.GetAudioClip("BDArmory/Sounds/armOff"); + heatGrowlSound = SoundUtils.GetAudioClip("BDArmory/Sounds/heatGrowl"); //HEAT LOCKING heatTarget = TargetSignatureData.noTarget; @@ -1039,7 +1202,9 @@ public void Start() BDArmorySetup.OnSavedSettings += ClampVisualRange; StartCoroutine(StartupListUpdater()); - missilesAway = 0; + firedMissiles = 0; + missilesAway = new Dictionary(); + rippleGunCount = new Dictionary(); GameEvents.onVesselCreate.Add(OnVesselCreate); GameEvents.onPartJointBreak.Add(OnPartJointBreak); @@ -1053,7 +1218,8 @@ public void Start() AI = VesselModuleRegistry.GetIBDAIControl(vessel, true); - RefreshModules(); + modulesNeedRefreshing = true; + cmPrioritiesNeedRefreshing = true; var SF = vessel.rootPart.FindModuleImplementing(); if (SF == null) { @@ -1067,11 +1233,12 @@ public void Start() GameEvents.onEditorPartDeleted.Add(UpdateMaxGunRange); UpdateMaxGunRange(part); } - targetingString = (targetCoM ? Localizer.Format("#LOC_BDArmory_TargetCOM") + "; " : "") - + (targetMass ? Localizer.Format("#LOC_BDArmory_Mass") + "; " : "") - + (targetCommand ? Localizer.Format("#LOC_BDArmory_Command") + "; " : "") - + (targetEngine ? Localizer.Format("#LOC_BDArmory_Engines") + "; " : "") - + (targetWeapon ? Localizer.Format("#LOC_BDArmory_Weapons") + "; " : ""); + targetingString = (targetCoM ? StringUtils.Localize("#LOC_BDArmory_TargetCOM") + "; " : "") + + (targetMass ? StringUtils.Localize("#LOC_BDArmory_Mass") + "; " : "") + + (targetCommand ? StringUtils.Localize("#LOC_BDArmory_Command") + "; " : "") + + (targetEngine ? StringUtils.Localize("#LOC_BDArmory_Engines") + "; " : "") + + (targetWeapon ? StringUtils.Localize("#LOC_BDArmory_Weapons") + "; " : "") + + (targetRandom ? StringUtils.Localize("#LOC_BDArmory_Random") + "; " : ""); } void OnPartDie() @@ -1092,18 +1259,20 @@ void OnPartDie(Part p) } catch (Exception e) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.MissileFire]: Error OnPartDie: " + e.Message); + //if (BDArmorySettings.DEBUG_LABELS) Debug.Log("[BDArmory.MissileFire]: Error OnPartDie: " + e.Message); Debug.Log("[BDArmory.MissileFire]: Error OnPartDie: " + e.Message); } } - RefreshModules(); - UpdateList(); + modulesNeedRefreshing = true; + weaponsListNeedsUpdating = true; + cmPrioritiesNeedRefreshing = true; + // UpdateList(); if (vessel != null) { var TI = vessel.gameObject.GetComponent(); if (TI != null) { - TI.UpdateTargetPartList(); + TI.targetPartListNeedsUpdating = true; } } } @@ -1111,7 +1280,8 @@ void OnPartDie(Part p) void OnVesselCreate(Vessel v) { if (v == null) return; - RefreshModules(); + modulesNeedRefreshing = true; + cmPrioritiesNeedRefreshing = true; } void OnPartJointBreak(PartJoint j, float breakForce) @@ -1126,10 +1296,12 @@ void OnPartJointBreak(PartJoint j, float breakForce) return; } - if ((j.Parent && j.Parent.vessel == vessel) || (j.Child && j.Child.vessel == vessel)) + if (HighLogic.LoadedSceneIsFlight && ((j.Parent && j.Parent.vessel == vessel) || (j.Child && j.Child.vessel == vessel))) { - RefreshModules(); - UpdateList(); + modulesNeedRefreshing = true; + weaponsListNeedsUpdating = true; + cmPrioritiesNeedRefreshing = true; + // UpdateList(); } } @@ -1171,80 +1343,46 @@ public override void OnUpdate() } base.OnUpdate(); - if (!vessel.packed) - { - if (weaponIndex >= weaponArray.Length) - { - hasSingleFired = true; - triggerTimer = 0; - - weaponIndex = Mathf.Clamp(weaponIndex, 0, weaponArray.Length - 1); - - SetDeployableWeapons(); - DisplaySelectedWeaponMessage(); - } - if (weaponArray.Length > 0 && selectedWeapon != weaponArray[weaponIndex]) - selectedWeapon = weaponArray[weaponIndex]; - - //finding next rocket to shoot (for aimer) - //FindNextRocket(); - - //targeting - if (weaponIndex > 0 && - (selectedWeapon.GetWeaponClass() == WeaponClasses.Missile || - selectedWeapon.GetWeaponClass() == WeaponClasses.SLW || - selectedWeapon.GetWeaponClass() == WeaponClasses.Bomb)) - { - SearchForLaserPoint(); - SearchForHeatTarget(); - SearchForRadarSource(); - } - - CalculateMissilesAway(); - } UpdateTargetingAudio(); - if (vessel.isActiveVessel) + if (vessel.isActiveVessel && !guardMode) // Manual firing. { + bool missileTriggerHeld = false; if (!CheckMouseIsOnGui() && isArmed && BDInputUtils.GetKey(BDInputSettingsFields.WEAP_FIRE_KEY)) { triggerTimer += Time.fixedDeltaTime; + missileTriggerHeld = true; } else { triggerTimer = 0; + } + if (BDInputUtils.GetKey(BDInputSettingsFields.WEAP_FIRE_MISSILE_KEY)) + { + FireMissileManually(false); + missileTriggerHeld = true; + } + if (hasSingleFired && !missileTriggerHeld) + { hasSingleFired = false; } + if (BDInputUtils.GetKeyDown(BDInputSettingsFields.WEAP_NEXT_KEY)) CycleWeapon(true); + if (BDInputUtils.GetKeyDown(BDInputSettingsFields.WEAP_PREV_KEY)) CycleWeapon(false); + if (BDInputUtils.GetKeyDown(BDInputSettingsFields.WEAP_TOGGLE_ARMED_KEY)) ToggleArm(); + if (BDInputUtils.GetKeyDown(BDInputSettingsFields.TGP_SELECT_NEXT_GPS_TARGET)) SelectNextGPSTarget(); //firing missiles and rockets=== - if (!guardMode && - selectedWeapon != null && + if (selectedWeapon != null && (selectedWeapon.GetWeaponClass() == WeaponClasses.Missile || selectedWeapon.GetWeaponClass() == WeaponClasses.Bomb || selectedWeapon.GetWeaponClass() == WeaponClasses.SLW )) { canRipple = true; - if (!MapView.MapIsEnabled && triggerTimer > BDArmorySettings.TRIGGER_HOLD_TIME && !hasSingleFired) - { - if (rippleFire) - { - if (Time.time - rippleTimer > 60f / rippleRPM) - { - FireMissile(); - rippleTimer = Time.time; - } - } - else - { - FireMissile(); - hasSingleFired = true; - } - } + FireMissileManually(true); } - else if (!guardMode && - selectedWeapon != null && + else if (selectedWeapon != null && ((selectedWeapon.GetWeaponClass() == WeaponClasses.Gun || selectedWeapon.GetWeaponClass() == WeaponClasses.Rocket || selectedWeapon.GetWeaponClass() == WeaponClasses.DefenseLaser) && currentGun.canRippleFire))//&& currentGun.roundsPerMinute < 1500)) //set this based on if the WG can ripple vs if first weapon in the WG happens to be > 1500 RPM @@ -1253,35 +1391,102 @@ public override void OnUpdate() } else { - canRipple = false; + canRipple = false; // Disable the ripple options in the WM gui. } } + else + { + canRipple = false; // Disable the ripple options in the WM gui. + triggerTimer = 0; + hasSingleFired = false; // The AI uses this as part of it's authorisation check for guns! + } + } + + void UpdateWeaponIndex() + { + if (weaponIndex >= weaponArray.Length) + { + hasSingleFired = true; + triggerTimer = 0; + + weaponIndex = Mathf.Clamp(weaponIndex, 0, weaponArray.Length - 1); + + SetDeployableWeapons(); + DisplaySelectedWeaponMessage(); + } + if (weaponArray.Length > 0 && selectedWeapon != weaponArray[weaponIndex]) + selectedWeapon = weaponArray[weaponIndex]; + + //finding next rocket to shoot (for aimer) + //FindNextRocket(); + } + + void UpdateGuidanceTargets() + { + if (weaponIndex > 0 && + (selectedWeapon.GetWeaponClass() == WeaponClasses.Missile || + selectedWeapon.GetWeaponClass() == WeaponClasses.SLW || + selectedWeapon.GetWeaponClass() == WeaponClasses.Bomb)) + { + SearchForLaserPoint(); + SearchForHeatTarget(); + SearchForRadarSource(); + } + CalculateMissilesAway(); } - private void CalculateMissilesAway() //for multi-missile targeting, would need to change this from a list of all missile fired; instead track missile launch vessel, and missiletarget - { //then ask for num of misiles fired at currentTarget, instead of total. And some code to swap targets if missilesAway > limit - int tempMissilesAway = 0; - using (List.Enumerator firedMissiles = BDATargetManager.FiredMissiles.GetEnumerator()) - while (firedMissiles.MoveNext()) + private void CalculateMissilesAway() //FIXME - add check for identically named vessels + { + missilesAway.Clear(); + // int tempMissilesAway = 0; + //firedMissiles = 0; + if (!guardMode) return; + using (List.Enumerator Missiles = BDATargetManager.FiredMissiles.GetEnumerator()) + while (Missiles.MoveNext()) { - if (firedMissiles.Current == null) continue; + if (Missiles.Current == null) continue; - var missileBase = firedMissiles.Current as MissileBase; + var missileBase = Missiles.Current as MissileBase; + if (missileBase.targetVessel == null) continue; if (missileBase.SourceVessel != this.vessel) continue; - - if (missileBase.MissileState != MissileBase.MissileStates.PostThrust && !missileBase.HasMissed && !missileBase.HasExploded) + //if (missileBase.MissileState != MissileBase.MissileStates.PostThrust && !missileBase.HasMissed && !missileBase.HasExploded) + if ((missileBase.HasFired || missileBase.launched) && !missileBase.HasMissed && !missileBase.HasExploded || missileBase.GetWeaponClass() == WeaponClasses.Bomb) //culling post-thrust missiles makes AGMs get cleared almost immediately after launch { - tempMissilesAway++; + if (!missilesAway.ContainsKey(missileBase.targetVessel)) + { + missilesAway.Add(missileBase.targetVessel, 1); + } + else + { + missilesAway[missileBase.targetVessel]++; //tabulate all missiles fired by the vessel at various targets; only need # missiles fired at current target forlaunching, but need all vessels with missiles targeting them for vessel targeting + } } } - - this.missilesAway = tempMissilesAway; + if (currentTarget != null && missilesAway.ContainsKey(currentTarget)) //change to previous target? + { + missilesAway.TryGetValue(currentTarget, out int missiles); + firedMissiles = missiles; + } + else + { + firedMissiles = 0; + } + if (!BDATargetManager.FiredMissiles.Contains(PreviousMissile)) PreviousMissile = null; + engagedTargets = missilesAway.Count; + //this.missilesAway = tempMissilesAway; } - public override void OnFixedUpdate() { if (vessel == null) return; + if (weaponsListNeedsUpdating) UpdateList(); + + if (!vessel.packed) + { + UpdateWeaponIndex(); + UpdateGuidanceTargets(); + } + if (guardMode && vessel.IsControllable) { GuardMode(); @@ -1313,14 +1518,15 @@ void ClampVisualRange() void OnGUI() { + if (!BDArmorySettings.DEBUG_LINES && lr != null) { lr.enabled = false; } if (HighLogic.LoadedSceneIsFlight && vessel == FlightGlobals.ActiveVessel && BDArmorySetup.GAME_UI_ENABLED && !MapView.MapIsEnabled) { - if (BDArmorySettings.DRAW_DEBUG_LINES) + if (BDArmorySettings.DEBUG_LINES) { if (incomingMissileVessel) { - BDGUIUtils.DrawLineBetweenWorldPositions(part.transform.position, + GUIUtils.DrawLineBetweenWorldPositions(part.transform.position, incomingMissileVessel.transform.position, 5, Color.cyan); } } @@ -1338,7 +1544,7 @@ void OnGUI() texture = BDArmorySetup.Instance.largeGreenCircleTexture; size = 256; } - BDGUIUtils.DrawTextureOnWorldPos(bombAimerPosition, texture, new Vector2(size, size), 0); + GUIUtils.DrawTextureOnWorldPos(bombAimerPosition, texture, new Vector2(size, size), 0); } } @@ -1350,16 +1556,19 @@ void OnGUI() { if (laserPointDetected && foundCam) { - BDGUIUtils.DrawTextureOnWorldPos(foundCam.groundTargetPosition, BDArmorySetup.Instance.greenCircleTexture, new Vector2(48, 48), 1); + GUIUtils.DrawTextureOnWorldPos(foundCam.groundTargetPosition, BDArmorySetup.Instance.greenCircleTexture, new Vector2(48, 48), 1); + } + else + { + GUIUtils.DrawTextureOnWorldPos(missile.MissileReferenceTransform.position + (2000 * missile.GetForwardTransform()), BDArmorySetup.Instance.largeGreenCircleTexture, new Vector2(96, 96), 0); } - using (List.Enumerator cam = BDATargetManager.ActiveLasers.GetEnumerator()) while (cam.MoveNext()) { if (cam.Current == null) continue; if (cam.Current.vessel != vessel && cam.Current.surfaceDetected && cam.Current.groundStabilized && !cam.Current.gimbalLimitReached) { - BDGUIUtils.DrawTextureOnWorldPos(cam.Current.groundTargetPosition, BDArmorySetup.Instance.greenDiamondTexture, new Vector2(18, 18), 0); + GUIUtils.DrawTextureOnWorldPos(cam.Current.groundTargetPosition, BDArmorySetup.Instance.greenDiamondTexture, new Vector2(18, 18), 0); } } } @@ -1368,17 +1577,17 @@ void OnGUI() MissileBase ml = CurrentMissile; if (heatTarget.exists) { - BDGUIUtils.DrawTextureOnWorldPos(heatTarget.position, BDArmorySetup.Instance.greenCircleTexture, new Vector2(36, 36), 3); + GUIUtils.DrawTextureOnWorldPos(heatTarget.position, BDArmorySetup.Instance.greenCircleTexture, new Vector2(36, 36), 3); float distanceToTarget = Vector3.Distance(heatTarget.position, ml.MissileReferenceTransform.position); - BDGUIUtils.DrawTextureOnWorldPos(ml.MissileReferenceTransform.position + (distanceToTarget * ml.GetForwardTransform()), BDArmorySetup.Instance.largeGreenCircleTexture, new Vector2(128, 128), 0); + GUIUtils.DrawTextureOnWorldPos(ml.MissileReferenceTransform.position + (distanceToTarget * ml.GetForwardTransform()), BDArmorySetup.Instance.largeGreenCircleTexture, new Vector2(128, 128), 0); Vector3 fireSolution = MissileGuidance.GetAirToAirFireSolution(ml, heatTarget.position, heatTarget.velocity); Vector3 fsDirection = (fireSolution - ml.MissileReferenceTransform.position).normalized; - BDGUIUtils.DrawTextureOnWorldPos(ml.MissileReferenceTransform.position + (distanceToTarget * fsDirection), BDArmorySetup.Instance.greenDotTexture, new Vector2(6, 6), 0); + GUIUtils.DrawTextureOnWorldPos(ml.MissileReferenceTransform.position + (distanceToTarget * fsDirection), BDArmorySetup.Instance.greenDotTexture, new Vector2(6, 6), 0); } else { - BDGUIUtils.DrawTextureOnWorldPos(ml.MissileReferenceTransform.position + (2000 * ml.GetForwardTransform()), BDArmorySetup.Instance.greenCircleTexture, new Vector2(36, 36), 3); - BDGUIUtils.DrawTextureOnWorldPos(ml.MissileReferenceTransform.position + (2000 * ml.GetForwardTransform()), BDArmorySetup.Instance.largeGreenCircleTexture, new Vector2(156, 156), 0); + GUIUtils.DrawTextureOnWorldPos(ml.MissileReferenceTransform.position + (2000 * ml.GetForwardTransform()), BDArmorySetup.Instance.greenCircleTexture, new Vector2(36, 36), 3); + GUIUtils.DrawTextureOnWorldPos(ml.MissileReferenceTransform.position + (2000 * ml.GetForwardTransform()), BDArmorySetup.Instance.largeGreenCircleTexture, new Vector2(156, 156), 0); } } else if (missile.TargetingMode == MissileBase.TargetingModes.Radar) @@ -1388,13 +1597,14 @@ void OnGUI() if (vesselRadarData && vesselRadarData.locked) { float distanceToTarget = Vector3.Distance(vesselRadarData.lockedTargetData.targetData.predictedPosition, ml.MissileReferenceTransform.position); - BDGUIUtils.DrawTextureOnWorldPos(ml.MissileReferenceTransform.position + (distanceToTarget * ml.GetForwardTransform()), BDArmorySetup.Instance.dottedLargeGreenCircle, new Vector2(128, 128), 0); + GUIUtils.DrawTextureOnWorldPos(ml.MissileReferenceTransform.position + (distanceToTarget * ml.GetForwardTransform()), BDArmorySetup.Instance.dottedLargeGreenCircle, new Vector2(128, 128), 0); //Vector3 fireSolution = MissileGuidance.GetAirToAirFireSolution(CurrentMissile, radar.lockedTarget.predictedPosition, radar.lockedTarget.velocity); Vector3 fireSolution = MissileGuidance.GetAirToAirFireSolution(ml, vesselRadarData.lockedTargetData.targetData.predictedPosition, vesselRadarData.lockedTargetData.targetData.velocity); Vector3 fsDirection = (fireSolution - ml.MissileReferenceTransform.position).normalized; - BDGUIUtils.DrawTextureOnWorldPos(ml.MissileReferenceTransform.position + (distanceToTarget * fsDirection), BDArmorySetup.Instance.greenDotTexture, new Vector2(6, 6), 0); + GUIUtils.DrawTextureOnWorldPos(ml.MissileReferenceTransform.position + (distanceToTarget * fsDirection), BDArmorySetup.Instance.greenDotTexture, new Vector2(6, 6), 0); - if (BDArmorySettings.DRAW_DEBUG_LABELS) + //if (BDArmorySettings.DEBUG_MISSILES) + if (BDArmorySettings.DEBUG_TELEMETRY) { string dynRangeDebug = string.Empty; MissileLaunchParams dlz = MissileLaunchParams.GetDynamicLaunchParams(missile, vesselRadarData.lockedTargetData.targetData.velocity, vesselRadarData.lockedTargetData.targetData.predictedPosition); @@ -1403,6 +1613,10 @@ void OnGUI() GUI.Label(new Rect(800, 600, 200, 200), dynRangeDebug); } } + else + { + GUIUtils.DrawTextureOnWorldPos(missile.MissileReferenceTransform.position + (2000 * missile.GetForwardTransform()), BDArmorySetup.Instance.largeGreenCircleTexture, new Vector2(96, 96), 0); + } } else if (missile.TargetingMode == MissileBase.TargetingModes.AntiRad) { @@ -1413,66 +1627,97 @@ void OnGUI() { if (rwr.pingsData[i].exists && (ml.antiradTargets.Contains(rwr.pingsData[i].signalStrength)) && Vector3.Dot(rwr.pingWorldPositions[i] - missile.transform.position, missile.GetForwardTransform()) > 0) { - BDGUIUtils.DrawTextureOnWorldPos(rwr.pingWorldPositions[i], BDArmorySetup.Instance.greenDiamondTexture, new Vector2(22, 22), 0); + GUIUtils.DrawTextureOnWorldPos(rwr.pingWorldPositions[i], BDArmorySetup.Instance.greenDiamondTexture, new Vector2(22, 22), 0); } } } if (antiRadTargetAcquired) { - BDGUIUtils.DrawTextureOnWorldPos(antiRadiationTarget, + GUIUtils.DrawTextureOnWorldPos(antiRadiationTarget, BDArmorySetup.Instance.openGreenSquare, new Vector2(22, 22), 0); } } + else if (missile.TargetingMode == MissileBase.TargetingModes.None && selectedWeapon.GetWeaponClass() != WeaponClasses.Bomb) + { + GUIUtils.DrawTextureOnWorldPos(missile.MissileReferenceTransform.position + (2000 * missile.GetForwardTransform()), BDArmorySetup.Instance.largeGreenCircleTexture, new Vector2(96, 96), 0); + } } if ((missile && missile.TargetingMode == MissileBase.TargetingModes.Gps) || BDArmorySetup.Instance.showingWindowGPS) { if (designatedGPSCoords != Vector3d.zero) { - BDGUIUtils.DrawTextureOnWorldPos(VectorUtils.GetWorldSurfacePostion(designatedGPSCoords, vessel.mainBody), BDArmorySetup.Instance.greenSpikedPointCircleTexture, new Vector2(22, 22), 0); + GUIUtils.DrawTextureOnWorldPos(VectorUtils.GetWorldSurfacePostion(designatedGPSCoords, vessel.mainBody), BDArmorySetup.Instance.greenSpikedPointCircleTexture, new Vector2(22, 22), 0); } } - if (BDArmorySettings.DRAW_DEBUG_LABELS) - { + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_MISSILES || BDArmorySettings.DEBUG_WEAPONS) debugString.Length = 0; - debugString.AppendLine("Missiles away: " + missilesAway); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_MISSILES) + { + debugString.AppendLine($"Missiles away: {firedMissiles}; targeted vessels: {engagedTargets}"); + if (missileIsIncoming) { foreach (var incomingMissile in results.incomingMissiles) - debugString.AppendLine("Incoming missile: " + (incomingMissile.vessel != null ? incomingMissile.vessel.vesselName + " @ " + incomingMissile.distance.ToString("0") + "m (" + incomingMissile.time.ToString("0.0") + "s)" : null)); + debugString.AppendLine($"Incoming missile: {(incomingMissile.vessel != null ? incomingMissile.vessel.vesselName + $" @ {incomingMissile.distance:0} m ({incomingMissile.time:0.0}s)" : null)}"); } - if (underAttack) debugString.AppendLine("Under attack from " + (incomingThreatVessel != null ? incomingThreatVessel.vesselName : null)); - if (underFire) debugString.AppendLine("Under fire from " + (priorGunThreatVessel != null ? priorGunThreatVessel.vesselName : null)); + if (underAttack) debugString.AppendLine($"Under attack from {(incomingThreatVessel != null ? incomingThreatVessel.vesselName : null)}"); + if (underFire) debugString.AppendLine($"Under fire from {(priorGunThreatVessel != null ? priorGunThreatVessel.vesselName : null)}"); if (isChaffing) debugString.AppendLine("Chaffing"); if (isFlaring) debugString.AppendLine("Flaring"); + if (isSmoking) debugString.AppendLine("Dropping Smoke"); if (isECMJamming) debugString.AppendLine("ECMJamming"); + if (isCloaking) debugString.AppendLine("Cloaking"); + } + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_WEAPONS) + { if (weaponArray != null) // Heat debugging { List weaponHeatDebugStrings = new List(); - List weaponLaserDebugStrings = new List(); + List weaponAimDebugStrings = new List(); HashSet validClasses = new HashSet { WeaponClasses.Gun, WeaponClasses.Rocket, WeaponClasses.DefenseLaser }; foreach (var weaponCandidate in weaponArray) { if (weaponCandidate == null || !validClasses.Contains(weaponCandidate.GetWeaponClass())) continue; var weapon = (ModuleWeapon)weaponCandidate; + if (weapon is null) continue; weaponHeatDebugStrings.Add(String.Format(" - {0}: heat: {1,6:F1}, max: {2}, overheated: {3}", weapon.shortName, weapon.heat, weapon.maxHeat, weapon.isOverheated)); - weaponLaserDebugStrings.Add(String.Format(" -Lead Offset: {0}, FinalAimTgt: {1}, tgt Position: {2}, pointingAtSelf: {3}, tgt CosAngle {4}, wpn CosAngle {5}, Wpn Autofire {6}, RoF {7}, MaxRoF {8}", weapon.GetLeadOffset(), weapon.finalAimTarget, weapon.targetPosition, weapon.pointingAtSelf, weapon.targetCosAngle, weapon.targetAdjustedMaxCosAngle, weapon.autoFire, weapon.roundsPerMinute, weapon.baseRPM)); + + weaponAimDebugStrings.Add($" - Target: {(weapon.visualTargetPart != null ? weapon.visualTargetPart.name : weapon.visualTargetVessel != null ? weapon.visualTargetVessel.vesselName : weapon.GPSTarget ? "GPS" : weapon.slaved ? "slaved" : weapon.radarTarget ? "radar" : weapon.atprAcquired ? "atpr" : "none")}, Lead Offset: {weapon.GetLeadOffset()}, FinalAimTgt: {weapon.finalAimTarget}, tgt Position: {weapon.targetPosition}, pointingAtSelf: {weapon.pointingAtSelf}, safeToFire: {weapon.safeToFire}, tgt CosAngle {weapon.targetCosAngle}, wpn CosAngle {weapon.targetAdjustedMaxCosAngle}, Wpn Autofire {weapon.autoFire}, target Radius {weapon.targetRadius}, RoF {weapon.roundsPerMinute}, MaxRoF {weapon.baseRPM}"); + + // weaponAimDebugStrings.Add($" - Target pos: {weapon.targetPosition.ToString("G3")}, vel: {weapon.targetVelocity.ToString("G4")}, acc: {weapon.targetAcceleration.ToString("G6")}"); + // weaponAimDebugStrings.Add($" - Target rel pos: {(weapon.targetPosition - weapon.fireTransforms[0].position).ToString("G3")} ({(weapon.targetPosition - weapon.fireTransforms[0].position).magnitude:F1}), rel vel: {(weapon.targetVelocity - weapon.part.rb.velocity).ToString("G4")}, rel acc: {((Vector3)(weapon.targetAcceleration - weapon.vessel.acceleration)).ToString("G6")}"); +#if DEBUG + if (weapon.visualTargetVessel != null && weapon.visualTargetVessel.loaded) weaponAimDebugStrings.Add($" - Visual target {(weapon.visualTargetPart is not null ? weapon.visualTargetPart.name : "CoM")} on {weapon.visualTargetVessel.vesselName}, distance: {(weapon.fireTransforms[0] != null ? (weapon.finalAimTarget - weapon.fireTransforms[0].position).magnitude : 0):F1}, radius: {weapon.targetRadius:F1} ({weapon.visualTargetVessel.GetBounds()}), max deviation: {weapon.maxDeviation}, firing tolerance: {weapon.FiringTolerance}"); + if (weapon.turret) weaponAimDebugStrings.Add($" - Turret: pitch: {weapon.turret.Pitch:F3}° ({weapon.turret.minPitch}°—{weapon.turret.maxPitch}°), yaw: {weapon.turret.Yaw:F3}° ({-weapon.turret.yawRange / 2f}°—{weapon.turret.yawRange / 2f}°)"); +#endif + } + float shots = 0; + float hits = 0; + float accuracy = 0; + if (BDACompetitionMode.Instance.Scores.ScoreData.ContainsKey(vessel.vesselName)) + { + hits = BDACompetitionMode.Instance.Scores.ScoreData[vessel.vesselName].hits; + shots = BDACompetitionMode.Instance.Scores.ScoreData[vessel.vesselName].shotsFired; + if (shots > 0) accuracy = hits / shots; } + weaponHeatDebugStrings.Add($" - Shots Fired: {shots}, Shots Hit: {hits}, Accuracy: {accuracy:F3}"); + if (weaponHeatDebugStrings.Count > 0) { debugString.AppendLine("Weapon Heat:\n" + string.Join("\n", weaponHeatDebugStrings)); - debugString.AppendLine("Aim debugging:\n" + string.Join("\n", weaponLaserDebugStrings)); + debugString.AppendLine("Aim debugging:\n" + string.Join("\n", weaponAimDebugStrings)); } } - GUI.Label(new Rect(200, Screen.height - 500, 600, 200), debugString.ToString()); + GUI.Label(new Rect(200, Screen.height - 700, Screen.width / 2 - 200, 16 * debugString.Length), debugString.ToString()); } } } bool CheckMouseIsOnGui() { - return Utils.CheckMouseIsOnGui(); + return GUIUtils.CheckMouseIsOnGui(); } #endregion KSP Events @@ -1481,7 +1726,7 @@ bool CheckMouseIsOnGui() IEnumerator StartupListUpdater() { - while (vessel.packed || !FlightGlobals.ready) + while (!FlightGlobals.ready || (vessel is not null && (vessel.packed || !vessel.loaded))) { yield return null; if (vessel.isActiveVessel) @@ -1496,10 +1741,10 @@ IEnumerator MissileWarningResetRoutine() { while (enabled) { - yield return new WaitUntil(() => missileIsIncoming); // Wait until missile is incoming. - if (BDArmorySettings.DRAW_DEBUG_LABELS) { Debug.Log("[BDArmory.MissileFire]: Triggering missile warning on " + vessel.vesselName); } - yield return new WaitUntil(() => Time.time - incomingMissileLastDetected > 1f); // Wait until 1s after no missiles are detected. - if (BDArmorySettings.DRAW_DEBUG_LABELS) { Debug.Log("[BDArmory.MissileFire]: Silencing missile warning on " + vessel.vesselName); } + yield return new WaitUntilFixed(() => missileIsIncoming); // Wait until missile is incoming. + if (BDArmorySettings.DEBUG_AI) { Debug.Log($"[BDArmory.MissileFire]: Triggering missile warning on {vessel.vesselName}"); } + yield return new WaitUntilFixed(() => Time.time - incomingMissileLastDetected > 1f); // Wait until 1s after no missiles are detected. + if (BDArmorySettings.DEBUG_AI) { Debug.Log($"[BDArmory.MissileFire]: Silencing missile warning on {vessel.vesselName}"); } missileIsIncoming = false; } } @@ -1509,9 +1754,9 @@ IEnumerator UnderFireRoutine() underFireLastNotified = Time.time; // Update the last notification. if (underFire) yield break; // Already under fire, we only want 1 timer. underFire = true; - if (BDArmorySettings.DRAW_DEBUG_LABELS) { Debug.Log("[BDArmory.MissileFire]: Triggering under fire warning on " + vessel.vesselName + " by " + priorGunThreatVessel.vesselName); } - yield return new WaitUntil(() => Time.time - underFireLastNotified > 1f); // Wait until 1s after being under fire. - if (BDArmorySettings.DRAW_DEBUG_LABELS) { Debug.Log("[BDArmory.MissileFire]: Silencing under fire warning on " + vessel.vesselName); } + if (BDArmorySettings.DEBUG_AI) { Debug.Log($"[BDArmory.MissileFire]: Triggering under fire warning on {vessel.vesselName} by {priorGunThreatVessel.vesselName}"); } + yield return new WaitUntilFixed(() => Time.time - underFireLastNotified > 1f); // Wait until 1s after being under fire. + if (BDArmorySettings.DEBUG_AI) { Debug.Log($"[BDArmory.MissileFire]: Silencing under fire warning on {vessel.vesselName}"); } underFire = false; priorGunThreatVessel = null; } @@ -1521,9 +1766,9 @@ IEnumerator UnderAttackRoutine() underAttackLastNotified = Time.time; // Update the last notification. if (underAttack) yield break; // Already under attack, we only want 1 timer. underAttack = true; - if (BDArmorySettings.DRAW_DEBUG_LABELS) { Debug.Log("[BDArmory.MissileFire]: Triggering under attack warning on " + vessel.vesselName + " by " + incomingThreatVessel.vesselName); } - yield return new WaitUntil(() => Time.time - underAttackLastNotified > 1f); // Wait until 3s after being under attack. - if (BDArmorySettings.DRAW_DEBUG_LABELS) { Debug.Log("[BDArmory.MissileFire]: Silencing under attack warning on " + vessel.vesselName); } + if (BDArmorySettings.DEBUG_AI) { Debug.Log($"[BDArmory.MissileFire]: Triggering under attack warning on {vessel.vesselName} by {incomingThreatVessel.vesselName}"); } + yield return new WaitUntilFixed(() => Time.time - underAttackLastNotified > 1f); // Wait until 3s after being under attack. + if (BDArmorySettings.DEBUG_AI) { Debug.Log($"[BDArmory.MissileFire]: Silencing under attack warning on {vessel.vesselName}"); } underAttack = false; } @@ -1531,7 +1776,7 @@ IEnumerator GuardTurretRoutine() { if (SetDeployableWeapons()) { - yield return new WaitForSeconds(2f); + yield return new WaitForSecondsFixed(2f); } if (gameObject.activeInHierarchy) @@ -1570,22 +1815,20 @@ IEnumerator GuardTurretRoutine() } else { - // DISABLE RADAR - /* - if (!vesselRadarData || !(vesselRadarData.radarCount > 0)) + // Turn on radars if off + if (!results.foundAntiRadiationMissile) { - List.Enumerator rd = radars.GetEnumerator(); - while (rd.MoveNext()) - { - if (rd.Current == null) continue; - if (!rd.Current.canLock) continue; - rd.Current.EnableRadar(); - break; - } - rd.Dispose(); + using (List.Enumerator rd = radars.GetEnumerator()) + while (rd.MoveNext()) + { + if (rd.Current != null || rd.Current.canLock) + { + rd.Current.EnableRadar(); + } + } } - */ + // Try to lock target, or if already locked, fire on it if (vesselRadarData && (!vesselRadarData.locked || (vesselRadarData.lockedTargetData.targetData.predictedPosition - guardTarget.transform.position) @@ -1593,7 +1836,7 @@ IEnumerator GuardTurretRoutine() { //vesselRadarData.TryLockTarget(guardTarget.transform.position); vesselRadarData.TryLockTarget(guardTarget); - yield return new WaitForSeconds(0.5f); + yield return new WaitForSecondsFixed(0.5f); if (guardTarget && vesselRadarData && vesselRadarData.locked && vesselRadarData.lockedTargetData.vessel == guardTarget) { @@ -1602,6 +1845,13 @@ IEnumerator GuardTurretRoutine() yield break; } } + else if (guardTarget && vesselRadarData && vesselRadarData.locked && + vesselRadarData.lockedTargetData.vessel == guardTarget) + { + vesselRadarData.SlaveTurrets(); + StartGuardTurretFiring(); + yield break; + } if (!guardTarget || (guardTarget.transform.position - transform.position).sqrMagnitude > guardRange * guardRange) { @@ -1617,7 +1867,7 @@ IEnumerator GuardTurretRoutine() IEnumerator ResetMissileThreatDistanceRoutine() { - yield return new WaitForSeconds(8); + yield return new WaitForSecondsFixed(8); incomingMissileDistance = float.MaxValue; incomingMissileTime = float.MaxValue; } @@ -1629,84 +1879,102 @@ IEnumerator GuardMissileRoutine() if (ml && !guardFiringMissile) { guardFiringMissile = true; + var wait = new WaitForFixedUpdate(); - if (ml.TargetingMode == MissileBase.TargetingModes.Radar && vesselRadarData) + if (ml.TargetingMode == MissileBase.TargetingModes.Radar) { - if (SetCargoBays()) + if (vesselRadarData) //no check for radar present, but off/out of juice { - yield return new WaitForSeconds(2f); - } - - float attemptLockTime = Time.time; - while ((!vesselRadarData.locked || (vesselRadarData.lockedTargetData.vessel != guardTarget)) && Time.time - attemptLockTime < 2) - { - if (vesselRadarData.locked) + float BayTriggerTime = -1; + if (SetCargoBays()) { - vesselRadarData.SwitchActiveLockedTarget(guardTarget); - yield return null; + BayTriggerTime = Time.time; + //yield return new WaitForSecondsFixed(2f); //so this doesn't delay radar targeting stuff below } - //vesselRadarData.TryLockTarget(guardTarget.transform.position+(guardTarget.rb_velocity*Time.fixedDeltaTime)); - vesselRadarData.TryLockTarget(guardTarget); - yield return new WaitForSeconds(0.25f); - } - // if (ml && AIMightDirectFire() && vesselRadarData.locked) - // { - // SetCargoBays(); - // float LAstartTime = Time.time; - // while (AIMightDirectFire() && Time.time - LAstartTime < 3 && !GetLaunchAuthorization(guardTarget, this)) - // { - // yield return new WaitForFixedUpdate(); - // } - // // yield return new WaitForSeconds(0.5f); - // } + float attemptLockTime = Time.time; + while ((!vesselRadarData.locked || (vesselRadarData.lockedTargetData.vessel != guardTarget)) && Time.time - attemptLockTime < 2) + { + if (vesselRadarData.locked) + { + vesselRadarData.SwitchActiveLockedTarget(guardTarget); //FIXME - this will cause issues if reviously fired a SARH with a single lock radar, then trying to fire another radar missile when MMPT > 1; wait until SARH hits? + yield return wait; //see about weighting SARH missiles lower when maxMissilesPerTgt > 1 and max supported radar locks is < than MMPT? + } + //vesselRadarData.TryLockTarget(guardTarget.transform.position+(guardTarget.rb_velocity*Time.fixedDeltaTime)); + else + { + vesselRadarData.TryLockTarget(guardTarget); + } + yield return new WaitForSecondsFixed(0.25f); + } - //wait for missile turret to point at target - //TODO BDModularGuidance: add turret - MissileLauncher mlauncher = ml as MissileLauncher; - if (mlauncher != null) - { - if (guardTarget && ml && mlauncher.missileTurret && vesselRadarData.locked) + // if (ml && AIMightDirectFire() && vesselRadarData.locked) + // { + // SetCargoBays(); + // float LAstartTime = Time.time; + // while (AIMightDirectFire() && Time.time - LAstartTime < 3 && !GetLaunchAuthorization(guardTarget, this)) + // { + // yield return new WaitForFixedUpdate(); + // } + // // yield return new WaitForSecondsFixed(0.5f); + // } + + //wait for missile turret to point at target + //TODO BDModularGuidance: add turret + MissileLauncher mlauncher = ml as MissileLauncher; + if (mlauncher != null) { - vesselRadarData.SlaveTurrets(); - float turretStartTime = Time.time; - while (Time.time - turretStartTime < 5) + if (guardTarget && ml && mlauncher.missileTurret && vesselRadarData.locked) { - float angle = Vector3.Angle(mlauncher.missileTurret.finalTransform.forward, mlauncher.missileTurret.slavedTargetPosition - mlauncher.missileTurret.finalTransform.position); - if (angle < mlauncher.missileTurret.fireFOV) + vesselRadarData.SlaveTurrets(); + float turretStartTime = Time.time; + while (Time.time - turretStartTime < 5) { - break; - // turretStartTime -= 2 * Time.fixedDeltaTime; + float angle = Vector3.Angle(mlauncher.missileTurret.finalTransform.forward, mlauncher.missileTurret.slavedTargetPosition - mlauncher.missileTurret.finalTransform.position); + if (angle < mlauncher.missileTurret.fireFOV) + { + break; + // turretStartTime -= 2 * Time.fixedDeltaTime; + } + yield return new WaitForFixedUpdate(); } - yield return new WaitForFixedUpdate(); } } - } - yield return null; + yield return wait; - // if (ml && guardTarget && vesselRadarData.locked && (!AIMightDirectFire() || GetLaunchAuthorization(guardTarget, this))) - if (ml && guardTarget && vesselRadarData.locked && GetLaunchAuthorization(guardTarget, this)) - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + // if (ml && guardTarget && vesselRadarData.locked && (!AIMightDirectFire() || GetLaunchAuthorization(guardTarget, this))) + if (ml && guardTarget && vesselRadarData.locked && vesselRadarData.lockedTargetData.vessel == guardTarget && GetLaunchAuthorization(guardTarget, this)) { - Debug.Log("[BDArmory.MissileFire]: " + vessel.vesselName + " firing on target " + guardTarget.GetName()); + if (BDArmorySettings.DEBUG_MISSILES) + { + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} firing on target {guardTarget.GetName()}"); + } + if (BayTriggerTime > 0 && (Time.time - BayTriggerTime < 2)) //if bays opening, see if 2 sec for the bays to open have elapsed, if not, wait remaining time needed + { + yield return new WaitForSecondsFixed(2 - (Time.time - BayTriggerTime)); + } + FireCurrentMissile(true); + //StartCoroutine(MissileAwayRoutine(mlauncher)); } - FireCurrentMissile(true); - //StartCoroutine(MissileAwayRoutine(mlauncher)); + } + else //no radar, missiles now expensive unguided ordinance + { + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName}'s {CurrentMissile.name} has no radar, attempting unguided fire."); + unguidedWeapon = true; //so let them be used as unguided ordinance } } else if (ml.TargetingMode == MissileBase.TargetingModes.Heat) { if (vesselRadarData && vesselRadarData.locked) // FIXME This wipes radar guided missiles' targeting data when switching to a heat guided missile. Radar is used to allow heat seeking missiles with allAspect = true to lock on target and fire when the target is not within sensor FOV { - vesselRadarData.UnlockAllTargets(); + vesselRadarData.UnlockAllTargets(); //maybe use vrd.UnlockCurrentTarget() instead? vesselRadarData.UnslaveTurrets(); } if (SetCargoBays()) { - yield return new WaitForSeconds(2f); + yield return new WaitForSecondsFixed(2f); } float attemptStartTime = Time.time; @@ -1739,9 +2007,14 @@ IEnumerator GuardMissileRoutine() { //vesselRadarData.TryLockTarget(guardTarget.transform.position); vesselRadarData.TryLockTarget(guardTarget); - yield return new WaitForSeconds(Mathf.Min(1, (targetScanInterval * 0.25f))); + yield return new WaitForSecondsFixed(Mathf.Min(1, (targetScanInterval * 0.25f))); } } + if (guardTarget && !heatTarget.exists && vesselRadarData && vesselRadarData.irstCount > 0) + { + heatTarget = vesselRadarData.activeIRTarget(); + yield return new WaitForSecondsFixed(Mathf.Min(1, (targetScanInterval * 0.25f))); + } // if (AIMightDirectFire() && ml && heatTarget.exists) // { @@ -1750,7 +2023,7 @@ IEnumerator GuardMissileRoutine() // { // yield return new WaitForFixedUpdate(); // } - // yield return new WaitForSeconds(0.5f); + // yield return new WaitForSecondsFixed(0.5f); // } //wait for missile turret to point at target @@ -1777,50 +2050,116 @@ IEnumerator GuardMissileRoutine() } } - yield return null; + yield return wait; // if (guardTarget && ml && heatTarget.exists && (!AIMightDirectFire() || GetLaunchAuthorization(guardTarget, this))) - if (guardTarget && ml && heatTarget.exists && GetLaunchAuthorization(guardTarget, this)) + if (guardTarget && ml && heatTarget.exists && heatTarget.vessel == guardTarget && GetLaunchAuthorization(guardTarget, this)) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_MISSILES) { - Debug.Log("[BDArmory.MissileFire]: " + vessel.vesselName + " firing on target " + guardTarget.GetName()); + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} firing on target {guardTarget.GetName()}"); } FireCurrentMissile(true); //StartCoroutine(MissileAwayRoutine(mlauncher)); } + //else //event that heatTarget.exists && heatTarget != guardtarget? } else if (ml.TargetingMode == MissileBase.TargetingModes.Gps) { designatedGPSInfo = new GPSTargetInfo(VectorUtils.WorldPositionToGeoCoords(guardTarget.CoM, vessel.mainBody), guardTarget.vesselName.Substring(0, Mathf.Min(12, guardTarget.vesselName.Length))); - - FireCurrentMissile(true); - //if (FireCurrentMissile(true)) - // StartCoroutine(MissileAwayRoutine(ml)); //NEW: try to prevent launching all missile complements at once... - } - else if (ml.TargetingMode == MissileBase.TargetingModes.AntiRad) - { - if (rwr) - { - if (!rwr.rwrEnabled) rwr.EnableRWR(); - if (rwr.rwrEnabled && !rwr.displayRWR) rwr.displayRWR = true; - } - if (SetCargoBays()) { - yield return new WaitForSeconds(2f); + yield return new WaitForSecondsFixed(2f); } - - float attemptStartTime = Time.time; - float attemptDuration = targetScanInterval * 0.75f; - while (Time.time - attemptStartTime < attemptDuration && - (!antiRadTargetAcquired || (antiRadiationTarget - guardTarget.CoM).sqrMagnitude > 20 * 20)) - { + MissileLauncher mlauncher; + mlauncher = ml as MissileLauncher; + if (mlauncher != null) + { + if (ml && mlauncher.missileTurret) + { + float turretStartTime = Time.time; + while (Time.time - turretStartTime < Mathf.Max(targetScanInterval / 2f, 2)) + { + float angle = Vector3.Angle(mlauncher.missileTurret.finalTransform.forward, mlauncher.missileTurret.slavedTargetPosition - mlauncher.missileTurret.finalTransform.position); + mlauncher.missileTurret.slaved = true; + mlauncher.missileTurret.slavedTargetPosition = MissileGuidance.GetAirToAirFireSolution(mlauncher, designatedGPSInfo.worldPos, designatedGPSInfo.gpsVessel.Velocity()); + mlauncher.missileTurret.SlavedAim(); + + if (angle < mlauncher.missileTurret.fireFOV) + { + break; + // turretStartTime -= 3 * Time.fixedDeltaTime; + } + yield return new WaitForFixedUpdate(); + } + } + } + yield return wait; + if (BDArmorySettings.DEBUG_MISSILES) + { + Debug.Log("[BDArmory.MissileFire]: {vessel.vesselName} firing GPS missile at {designatedGPSInfo.worldPos}"); + } + FireCurrentMissile(true); + //if (FireCurrentMissile(true)) + // StartCoroutine(MissileAwayRoutine(ml)); //NEW: try to prevent launching all missile complements at once... + } + else if (ml.TargetingMode == MissileBase.TargetingModes.AntiRad) + { + if (rwr) + { + if (!rwr.rwrEnabled) rwr.EnableRWR(); + if (rwr.rwrEnabled && !rwr.displayRWR) rwr.displayRWR = true; + } + + if (SetCargoBays()) + { + yield return new WaitForSecondsFixed(2f); + } + + float attemptStartTime = Time.time; + float attemptDuration = targetScanInterval * 0.75f; + MissileLauncher mlauncher; + while (Time.time - attemptStartTime < attemptDuration && + (!antiRadTargetAcquired || !AntiRadDistanceCheck())) + { + mlauncher = ml as MissileLauncher; + if (mlauncher != null) + { + if (mlauncher.missileTurret) + { + mlauncher.missileTurret.slaved = true; + mlauncher.missileTurret.slavedTargetPosition = guardTarget.CoM; + mlauncher.missileTurret.SlavedAim(); + } + } yield return new WaitForFixedUpdate(); } + mlauncher = ml as MissileLauncher; + if (mlauncher != null) + { + if (ml && mlauncher.missileTurret && antiRadTargetAcquired) + { + float turretStartTime = attemptStartTime; + while (antiRadTargetAcquired && Time.time - turretStartTime < Mathf.Max(targetScanInterval / 2f, 2)) + { + float angle = Vector3.Angle(mlauncher.missileTurret.finalTransform.forward, mlauncher.missileTurret.slavedTargetPosition - mlauncher.missileTurret.finalTransform.position); + mlauncher.missileTurret.slaved = true; + mlauncher.missileTurret.slavedTargetPosition = MissileGuidance.GetAirToAirFireSolution(mlauncher, antiRadiationTarget, guardTarget.Velocity()); + mlauncher.missileTurret.SlavedAim(); - if (ml && antiRadTargetAcquired && (antiRadiationTarget - guardTarget.CoM).sqrMagnitude < 20 * 20) + if (angle < mlauncher.missileTurret.fireFOV) + { + break; + // turretStartTime -= 3 * Time.fixedDeltaTime; + } + yield return new WaitForFixedUpdate(); + } + } + } + + yield return wait; + if (ml && antiRadTargetAcquired && AntiRadDistanceCheck()) { FireCurrentMissile(true); //StartCoroutine(MissileAwayRoutine(ml)); @@ -1830,7 +2169,7 @@ IEnumerator GuardMissileRoutine() { if (SetCargoBays()) { - yield return new WaitForSeconds(2f); + yield return new WaitForSecondsFixed(2f); } if (targetingPods.Count > 0) //if targeting pods are available, slew them onto target and lock. @@ -1839,7 +2178,6 @@ IEnumerator GuardMissileRoutine() while (tgp.MoveNext()) { if (tgp.Current == null) continue; - tgp.Current.EnableCamera(); tgp.Current.CoMLock = true; yield return StartCoroutine(tgp.Current.PointToPositionRoutine(guardTarget.CoM)); //if (tgp.Current.groundStabilized && (tgp.Current.GroundtargetPosition - guardTarget.transform.position).sqrMagnitude < 20 * 20) @@ -1850,6 +2188,10 @@ IEnumerator GuardMissileRoutine() //} } } + else //no cam, laser missiles now expensive unguided ordinance + { + unguidedWeapon = true; //so let them be used as unguided ordinance + } //search for a laser point that corresponds with target vessel float attemptStartTime = Time.time; @@ -1859,6 +2201,30 @@ IEnumerator GuardMissileRoutine() yield return new WaitForFixedUpdate(); } + MissileLauncher mlauncher = ml as MissileLauncher; + if (mlauncher != null) + { + if (guardTarget && ml && mlauncher.missileTurret && laserPointDetected) + { + //foundCam.SlaveTurrets(); + float turretStartTime = attemptStartTime; + while (Time.time - turretStartTime < Mathf.Max(targetScanInterval / 2f, 2)) + { + float angle = Vector3.Angle(mlauncher.missileTurret.finalTransform.forward, mlauncher.missileTurret.slavedTargetPosition - mlauncher.missileTurret.finalTransform.position); + mlauncher.missileTurret.slaved = true; + mlauncher.missileTurret.slavedTargetPosition = foundCam.groundTargetPosition; + mlauncher.missileTurret.SlavedAim(); + if (angle < mlauncher.missileTurret.fireFOV) + { + break; + // turretStartTime -= 2 * Time.fixedDeltaTime; + } + yield return new WaitForFixedUpdate(); + } + } + } + yield return wait; + if (ml && laserPointDetected && foundCam && (foundCam.groundTargetPosition - guardTarget.CoM).sqrMagnitude < 10 * 10) { FireCurrentMissile(true); @@ -1866,9 +2232,28 @@ IEnumerator GuardMissileRoutine() } else { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.MissileFire]: Laser Target Error"); + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.MissileFire]: Laser Target Error"); } } + else if (ml.TargetingMode == MissileBase.TargetingModes.None) + { + unguidedWeapon = true; + } + if (unguidedWeapon) //unguidedWeapon + { + if (SetCargoBays()) + { + yield return new WaitForSecondsFixed(2f); + } + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} attempting to fire unguided missile on target {guardTarget.GetName()}"); + + if (ml && guardTarget && GetLaunchAuthorization(guardTarget, this)) + { + FireCurrentMissile(true); + unguidedWeapon = false; + } + } + guardFiringMissile = false; } } @@ -1880,7 +2265,8 @@ IEnumerator GuardBombRoutine() float bombStartTime = Time.time; float bombAttemptDuration = Mathf.Max(targetScanInterval, 12f); float radius = CurrentMissile.GetBlastRadius() * Mathf.Min((1 + (maxMissilesOnTarget / 2f)), 1.5f); - if (CurrentMissile.TargetingMode == MissileBase.TargetingModes.Gps && (designatedGPSInfo.worldPos - guardTarget.CoM).sqrMagnitude > CurrentMissile.GetBlastRadius() * CurrentMissile.GetBlastRadius()) + if ((CurrentMissile.TargetingMode == MissileBase.TargetingModes.Gps && (designatedGPSInfo.worldPos - guardTarget.CoM).sqrMagnitude > CurrentMissile.GetBlastRadius() * CurrentMissile.GetBlastRadius()) + || (CurrentMissile.TargetingMode == MissileBase.TargetingModes.Laser && (!laserPointDetected || (foundCam && (foundCam.groundTargetPosition - guardTarget.CoM).sqrMagnitude > CurrentMissile.GetBlastRadius() * CurrentMissile.GetBlastRadius())))) { //check database for target first float twoxsqrRad = 4f * radius * radius; @@ -1897,39 +2283,31 @@ IEnumerator GuardBombRoutine() //no target in gps database, acquire via targeting pod if (!foundTargetInDatabase) { - ModuleTargetingCamera tgp = null; - using (List.Enumerator t = targetingPods.GetEnumerator()) - while (t.MoveNext()) - { - if (t.Current) tgp = t.Current; - } - - if (tgp != null) + if (targetingPods.Count > 0) { - tgp.EnableCamera(); - yield return StartCoroutine(tgp.PointToPositionRoutine(guardTarget.CoM)); - - if (tgp) - { - if (guardTarget && tgp.groundStabilized && (tgp.targetPointPosition - guardTarget.transform.position).sqrMagnitude < CurrentMissile.GetBlastRadius() * CurrentMissile.GetBlastRadius()) //was tgp.groundtargetposition - { - radius = 500; - designatedGPSInfo = new GPSTargetInfo(tgp.bodyRelativeGTP, "Guard Target"); - bombStartTime = Time.time; - } - else//failed to acquire target via tgp, cancel. + using (List.Enumerator tgp = targetingPods.GetEnumerator()) + while (tgp.MoveNext()) { - tgp.DisableCamera(); - designatedGPSInfo = new GPSTargetInfo(); - guardFiringMissile = false; - yield break; + if (tgp.Current == null) continue; + tgp.Current.EnableCamera(); + tgp.Current.CoMLock = true; + yield return StartCoroutine(tgp.Current.PointToPositionRoutine(guardTarget.CoM)); + + if (guardTarget && tgp.Current.groundStabilized && (tgp.Current.targetPointPosition - guardTarget.transform.position).sqrMagnitude < CurrentMissile.GetBlastRadius() * CurrentMissile.GetBlastRadius()) //was tgp.groundtargetposition + { + radius = 500; + designatedGPSInfo = new GPSTargetInfo(tgp.Current.bodyRelativeGTP, "Guard Target"); + bombStartTime = Time.time; + } + else//failed to acquire target via tgp, cancel. + { + tgp.Current.DisableCamera(); + designatedGPSInfo = new GPSTargetInfo(); + guardFiringMissile = false; + yield break; + } + } - } - else//no gps target and lost tgp, cancel. - { - guardFiringMissile = false; - yield break; - } } else //no gps target and no tgp, cancel. { @@ -1943,8 +2321,10 @@ IEnumerator GuardBombRoutine() float prevDist = 2 * radius; radius = Mathf.Max(radius, 50f); + var wait = new WaitForFixedUpdate(); + while (guardTarget && Time.time - bombStartTime < bombAttemptDuration && weaponIndex > 0 && - weaponArray[weaponIndex].GetWeaponClass() == WeaponClasses.Bomb && missilesAway < maxMissilesOnTarget) + weaponArray[weaponIndex].GetWeaponClass() == WeaponClasses.Bomb && firedMissiles < maxMissilesOnTarget) { float targetDist = Vector3.Distance(bombAimerPosition, guardTarget.CoM); @@ -1960,10 +2340,10 @@ IEnumerator GuardBombRoutine() if (targetDist < Mathf.Max(radius * 2, 800f) && Vector3.Dot(guardTarget.CoM - bombAimerPosition, guardTarget.CoM - transform.position) < 0) { - pilotAI.RequestExtend("too close to bomb", guardTarget); // Extend from target vessel. + pilotAI.RequestExtend("too close to bomb", guardTarget, ignoreCooldown: true); // Extend from target vessel. break; } - yield return null; + yield return wait; } else { @@ -1983,19 +2363,19 @@ IEnumerator GuardBombRoutine() { FireCurrentMissile(true); timeBombReleased = Time.time; - yield return new WaitForSeconds(rippleFire ? 60f / rippleRPM : 0.06f); - if (missilesAway >= maxMissilesOnTarget) + yield return new WaitForSecondsFixed(rippleFire ? 60f / rippleRPM : 0.06f); + if (firedMissiles >= maxMissilesOnTarget) { - yield return new WaitForSeconds(1f); + yield return new WaitForSecondsFixed(1f); if (pilotAI) { - pilotAI.RequestExtend("bombs away!", null, guardTarget.CoM); // Extend from the place the bomb is expected to fall. - } + pilotAI.RequestExtend("bombs away!", null, radius, guardTarget.CoM, ignoreCooldown: true); // Extend from the place the bomb is expected to fall. + } //maybe something similar should be adapted for any missiles with nuke warheards...? } } else { - yield return null; + yield return wait; } } } @@ -2097,6 +2477,14 @@ void UpdateTargetingAudio() if (selectedWeapon != null && selectedWeapon.GetWeaponClass() == WeaponClasses.Missile && vessel.isActiveVessel) { MissileBase ml = CurrentMissile; + if (ml == null) + { + if (targetingAudioSource.isPlaying) + { + targetingAudioSource.Stop(); + } + return; + } if (ml.TargetingMode == MissileBase.TargetingModes.Heat) { if (targetingAudioSource.clip != heatGrowlSound) @@ -2138,7 +2526,11 @@ void UpdateTargetingAudio() IEnumerator WarningSoundRoutine(float distance, MissileBase ml)//give distance parameter { - if (distance < this.guardRange) + bool detectedLaunch = false; + if (rwr && (rwr.omniDetection || (!rwr.omniDetection && ml.TargetingMode == MissileBase.TargetingModes.Radar) || irsts.Count > 0)) //omni RWR detection, radar spike from lock, or IR spike from launch + detectedLaunch = true; + + if (distance < (detectedLaunch ? this.guardRange : this.guardRange / 3)) { warningSounding = true; BDArmorySetup.Instance.missileWarningTime = Time.time; @@ -2148,9 +2540,9 @@ IEnumerator WarningSoundRoutine(float distance, MissileBase ml)//give distance p float waitTime = distance < 800 ? .25f : 1.5f; - yield return new WaitForSeconds(waitTime); + yield return new WaitForSecondsFixed(waitTime); - if (ml.vessel && CanSeeTarget(ml.vessel)) + if (ml.vessel && CanSeeTarget(ml)) { BDATargetManager.ReportVessel(ml.vessel, this); } @@ -2164,7 +2556,9 @@ IEnumerator WarningSoundRoutine(float distance, MissileBase ml)//give distance p public bool isChaffing; public bool isFlaring; + public bool isSmoking; public bool isECMJamming; + public bool isCloaking; bool isLegacyCMing; @@ -2173,7 +2567,7 @@ IEnumerator WarningSoundRoutine(float distance, MissileBase ml)//give distance p public void FireAllCountermeasures(int count) { - if (!isChaffing && !isFlaring && ThreatClosingTime(incomingMissileVessel) > cmThreshold) + if (!isChaffing && !isFlaring && !isSmoking && ThreatClosingTime(incomingMissileVessel) > cmThreshold) { StartCoroutine(AllCMRoutine(count)); } @@ -2187,6 +2581,14 @@ public void FireECM() } } + public void FireOCM(bool thermalthreat) + { + if (!isCloaking) + { + StartCoroutine(CloakRoutine(thermalthreat)); + } + } + public void FireChaff() { if (!isChaffing && ThreatClosingTime(incomingMissileVessel) <= cmThreshold) @@ -2204,73 +2606,112 @@ public void FireFlares() } } + public void FireSmoke() + { + if (!isSmoking && ThreatClosingTime(incomingMissileVessel) <= cmThreshold) + { + StartCoroutine(SmokeRoutine((int)chaffRepetition, chaffInterval)); + } + } + IEnumerator ECMRoutine() { isECMJamming = true; - //yield return new WaitForSeconds(UnityEngine.Random.Range(0.2f, 1f)); + //yield return new WaitForSecondsFixed(UnityEngine.Random.Range(0.2f, 1f)); using (var ecm = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) while (ecm.MoveNext()) { if (ecm.Current == null) continue; - if (ecm.Current.jammerEnabled) continue; + if (ecm.Current.manuallyEnabled) continue; + if (ecm.Current.jammerEnabled) + { + ecm.Current.manuallyEnabled = true; + continue; + } ecm.Current.EnableJammer(); } - yield return new WaitForSeconds(10.0f); + yield return new WaitForSecondsFixed(10.0f); isECMJamming = false; using (var ecm1 = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) while (ecm1.MoveNext()) { if (ecm1.Current == null) continue; - ecm1.Current.DisableJammer(); + if (!ecm1.Current.manuallyEnabled) + ecm1.Current.DisableJammer(); + } + } + + IEnumerator CloakRoutine(bool thermalthreat) + { + //Debug.Log("[Cloaking] under fire! cloaking!"); + + using (var ocm = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) + while (ocm.MoveNext()) + { + if (ocm.Current == null) continue; + if (ocm.Current.cloakEnabled) continue; + if (thermalthreat && ocm.Current.thermalReductionFactor >= 1) continue; //don't bother activating non-thermoptic camo when incoming heatseekers + if (!thermalthreat && ocm.Current.opticalReductionFactor >= 1) continue; //similarly, don't activate purely thermal cloaking systems if under gunfrire + isCloaking = true; + ocm.Current.EnableCloak(); + } + yield return new WaitForSecondsFixed(10.0f); + isCloaking = false; + + using (var ocm1 = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) + while (ocm1.MoveNext()) + { + if (ocm1.Current == null) continue; + ocm1.Current.DisableCloak(); } } IEnumerator ChaffRoutine(int repetition, float interval) { isChaffing = true; - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.MissileFire]: " + vessel.vesselName + " starting chaff routine"); - // yield return new WaitForSeconds(0.2f); // Reaction time delay - for (int i = 0; i < repetition; i++) + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} starting chaff routine"); + // yield return new WaitForSecondsFixed(0.2f); // Reaction time delay + for (int i = 0; i < repetition; ++i) { - using (var cm = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) - while (cm.MoveNext()) - { - if (cm.Current == null) continue; - if (cm.Current.cmType == CMDropper.CountermeasureTypes.Chaff) - { - cm.Current.DropCM(); - } - } - - yield return new WaitForSeconds(interval); + DropCM(CMDropper.CountermeasureTypes.Chaff); + if (i < repetition - 1) // Don't wait on the last one. + yield return new WaitForSecondsFixed(interval); } - yield return new WaitForSeconds(chaffWaitTime); + yield return new WaitForSecondsFixed(chaffWaitTime); isChaffing = false; - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.MissileFire]: " + vessel.vesselName + " ending chaff routine"); + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} ending chaff routine"); } IEnumerator FlareRoutine(int repetition, float interval) { isFlaring = true; - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.MissileFire]: " + vessel.vesselName + " starting flare routine"); - // yield return new WaitForSeconds(0.2f); // Reaction time delay - for (int i = 0; i < repetition; i++) + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} starting flare routine"); + // yield return new WaitForSecondsFixed(0.2f); // Reaction time delay + for (int i = 0; i < repetition; ++i) { - using (var cm = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) - while (cm.MoveNext()) - { - if (cm.Current == null) continue; - if (cm.Current.cmType == CMDropper.CountermeasureTypes.Flare) - { - cm.Current.DropCM(); - } - } - yield return new WaitForSeconds(interval); + DropCM(CMDropper.CountermeasureTypes.Flare); + if (i < repetition - 1) // Don't wait on the last one. + yield return new WaitForSecondsFixed(interval); } - yield return new WaitForSeconds(cmWaitTime); + yield return new WaitForSecondsFixed(cmWaitTime); isFlaring = false; - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.MissileFire]: " + vessel.vesselName + " ending flare routine"); + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} ending flare routine"); + } + IEnumerator SmokeRoutine(int repetition, float interval) + { + isSmoking = true; + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} starting smoke routine"); + // yield return new WaitForSecondsFixed(0.2f); // Reaction time delay + for (int i = 0; i < repetition; ++i) + { + DropCM(CMDropper.CountermeasureTypes.Smoke); + if (i < repetition - 1) // Don't wait on the last one. + yield return new WaitForSecondsFixed(interval); + } + yield return new WaitForSecondsFixed(cmWaitTime); + isSmoking = false; + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} ending smoke routine"); } IEnumerator AllCMRoutine(int count) @@ -2278,31 +2719,24 @@ IEnumerator AllCMRoutine(int count) // Use this routine for missile threats that are outside of the cmThreshold isFlaring = true; isChaffing = true; - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.MissileFire]: " + vessel.vesselName + " starting All CM routine"); - for (int i = 0; i < count; i++) + isSmoking = true; + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} starting All CM routine"); + for (int i = 0; i < count; ++i) { - using (var cm = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) - while (cm.MoveNext()) - { - if (cm.Current == null) continue; - if ((cm.Current.cmType == CMDropper.CountermeasureTypes.Flare) - || (cm.Current.cmType == CMDropper.CountermeasureTypes.Chaff) - || (cm.Current.cmType == CMDropper.CountermeasureTypes.Smoke)) - { - cm.Current.DropCM(); - } - } - yield return new WaitForSeconds(1f); + DropCMs((int)(CMDropper.CountermeasureTypes.Flare | CMDropper.CountermeasureTypes.Chaff | CMDropper.CountermeasureTypes.Smoke)); + if (i < count - 1) // Don't wait on the last one. + yield return new WaitForSecondsFixed(1f); } isFlaring = false; isChaffing = false; - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.MissileFire]: " + vessel.vesselName + " ending All CM routine"); + isSmoking = false; + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} ending All CM routine"); } IEnumerator LegacyCMRoutine() { isLegacyCMing = true; - yield return new WaitForSeconds(UnityEngine.Random.Range(.2f, 1f)); + yield return new WaitForSecondsFixed(UnityEngine.Random.Range(.2f, 1f)); if (incomingMissileDistance < 2500) { cmAmount = Mathf.RoundToInt((2500 - incomingMissileDistance) / 400); @@ -2315,17 +2749,63 @@ IEnumerator LegacyCMRoutine() cmCounter++; if (cmCounter < cmAmount) { - yield return new WaitForSeconds(0.15f); + yield return new WaitForSecondsFixed(0.15f); } else { cmCounter = 0; - yield return new WaitForSeconds(UnityEngine.Random.Range(.5f, 1f)); + yield return new WaitForSecondsFixed(UnityEngine.Random.Range(.5f, 1f)); } } isLegacyCMing = false; } + Dictionary cmCurrentPriorities = new Dictionary(); + void RefreshCMPriorities() + { + cmCurrentPriorities.Clear(); + foreach (var cm in VesselModuleRegistry.GetModules(vessel)) + { + if (cm == null) continue; + if (!cmCurrentPriorities.ContainsKey(cm.cmType) || cm.Priority > cmCurrentPriorities[cm.cmType]) + cmCurrentPriorities[cm.cmType] = cm.Priority; + } + cmPrioritiesNeedRefreshing = false; + } + + void DropCM(CMDropper.CountermeasureTypes cmType) => DropCMs((int)cmType); + void DropCMs(int cmTypes) + { + if (cmPrioritiesNeedRefreshing) RefreshCMPriorities(); // Refresh highest priorities if needed. + var cmDropped = cmCurrentPriorities.ToDictionary(kvp => kvp.Key, kvp => false); + // Drop the appropriate CMs. + foreach (var cm in VesselModuleRegistry.GetModules(vessel)) + { + if (cm == null) continue; + if (((int)cm.cmType & cmTypes) != 0 && cm.Priority == cmCurrentPriorities[cm.cmType]) + { + if (cm.DropCM()) + cmDropped[cm.cmType] = true; + } + } + // Check for not having dropped any of the current priority. + foreach (var cmType in cmDropped.Keys) + { + if (((int)cmType & cmTypes) == 0) continue; // This type wasn't requested. + if (cmDropped[cmType]) continue; // Successfully dropped something of this type. + if (cmCurrentPriorities[cmType] > -1) + { + --cmCurrentPriorities[cmType]; // Lower the priority. + if (cmCurrentPriorities[cmType] > -1) // Still some left? + DropCMs((int)cmType); // Fire some of the next priority. + } + } + } + + [KSPAction("#LOC_BDArmory_FireCountermeasure")]//Fire Countermeasure + public void AGDropCMs(KSPActionParam param) + { DropCMs((int)(CMDropper.CountermeasureTypes.Flare | CMDropper.CountermeasureTypes.Chaff | CMDropper.CountermeasureTypes.Smoke)); } + public void MissileWarning(float distance, MissileBase ml)//take distance parameter { if (vessel.isActiveVessel && !warningSounding) @@ -2333,10 +2813,10 @@ public void MissileWarning(float distance, MissileBase ml)//take distance parame StartCoroutine(WarningSoundRoutine(distance, ml)); } - if (BDArmorySettings.DRAW_DEBUG_LABELS && distance < 1000f) Debug.Log("[BDArmory.MissileFire]: Legacy missile warning for " + vessel.vesselName + " at distance " + distance.ToString("0.0") + "m from " + ml.shortName); - missileIsIncoming = true; - incomingMissileLastDetected = Time.time; - incomingMissileDistance = distance; + //if (BDArmorySettings.DEBUG_LABELS && distance < 1000f) Debug.Log("[BDArmory.MissileFire]: Legacy missile warning for " + vessel.vesselName + " at distance " + distance.ToString("0.0") + "m from " + ml.shortName); + //missileIsIncoming = true; + //incomingMissileLastDetected = Time.time; + //incomingMissileDistance = distance; } #endregion CounterMeasure @@ -2347,11 +2827,11 @@ bool FireCurrentMissile(bool checkClearance) { MissileBase missile = CurrentMissile; if (missile == null) return false; - + bool DisengageAfterFiring = false; if (missile is MissileBase) { MissileBase ml = missile; - if (checkClearance && (!CheckBombClearance(ml) || (ml is MissileLauncher && ((MissileLauncher)ml).rotaryRail && !((MissileLauncher)ml).rotaryRail.readyMissile == ml))) + if (checkClearance && (!CheckBombClearance(ml) || (ml is MissileLauncher && ((MissileLauncher)ml).rotaryRail && !((MissileLauncher)ml).rotaryRail.readyMissile == ml)) || ml.launched) { using (var otherMissile = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) while (otherMissile.MoveNext()) @@ -2359,6 +2839,8 @@ bool FireCurrentMissile(bool checkClearance) if (otherMissile.Current == null) continue; if (otherMissile.Current == ml || otherMissile.Current.GetShortName() != ml.GetShortName() || !CheckBombClearance(otherMissile.Current)) continue; + if (otherMissile.Current.GetEngagementRangeMax() != selectedWeaponsEngageRangeMax) continue; + if (otherMissile.Current.launched) continue; CurrentMissile = otherMissile.Current; selectedWeapon = otherMissile.Current; FireCurrentMissile(false); @@ -2366,9 +2848,9 @@ bool FireCurrentMissile(bool checkClearance) } CurrentMissile = ml; selectedWeapon = ml; + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileFire]: No Clearance! Cannot fire {CurrentMissile.GetShortName()}"); return false; } - if (ml is MissileLauncher && ((MissileLauncher)ml).missileTurret) { ((MissileLauncher)ml).missileTurret.FireMissile(((MissileLauncher)ml)); @@ -2385,6 +2867,7 @@ bool FireCurrentMissile(bool checkClearance) { SendTargetDataToMissile(ml); ml.FireMissile(); + PreviousMissile = ml; } if (guardMode) @@ -2393,6 +2876,18 @@ bool FireCurrentMissile(bool checkClearance) { //StartCoroutine(BombsAwayRoutine(ml)); } + if (ml.warheadType == MissileBase.WarheadTypes.EMP || ml.warheadType == MissileBase.WarheadTypes.Nuke) + { + MissileLauncher cm = missile as MissileLauncher; + float thrust = cm == null ? 30 : cm.thrust; + float timeToImpact = AIUtils.TimeToCPA(guardTarget, vessel.CoM, vessel.Velocity(), (thrust / missile.part.mass) * missile.GetForwardTransform(), 16); + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileFire]: Blast standoff dist: {ml.StandOffDistance}; time2Impact: {timeToImpact}"); + if (ml.StandOffDistance > 0 && Vector3.Distance(transform.position + (vessel.Velocity() * timeToImpact), currentTarget.position + currentTarget.velocity) > ml.StandOffDistance) //if predicted craft position will be within blast radius when missile arrives, break off + { + DisengageAfterFiring = true; + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.MissileFire]: Need to withdraw from projected blast zone!"); + } + } } else { @@ -2406,10 +2901,18 @@ bool FireCurrentMissile(bool checkClearance) { SendTargetDataToMissile(missile); missile.FireMissile(); + PreviousMissile = missile; } - CalculateMissilesAway(); // Immediately update missiles away. + //PreviousMissile = CurrentMissile; UpdateList(); + if (DisengageAfterFiring) + { + if (pilotAI) + { + pilotAI.RequestExtend("Nuke away!", guardTarget, missile.StandOffDistance * 1.25f, guardTarget.CoM, ignoreCooldown: true); // Extend from projected detonation site if within blast radius + } + } return true; } @@ -2424,19 +2927,42 @@ void FireMissile() { return; } - if (guardMode && (missilesAway >= maxMissilesOnTarget)) + if (guardMode && (firedMissiles >= maxMissilesOnTarget)) { return; } if (selectedWeapon.GetWeaponClass() == WeaponClasses.Missile || - selectedWeapon.GetWeaponClass() == WeaponClasses.SLW || - selectedWeapon.GetWeaponClass() == WeaponClasses.Bomb) + selectedWeapon.GetWeaponClass() == WeaponClasses.SLW || + selectedWeapon.GetWeaponClass() == WeaponClasses.Bomb) { FireCurrentMissile(true); } UpdateList(); } + /// + /// Fire a missile via trigger, action group or hotkey. + /// + void FireMissileManually(bool mainTrigger) + { + if (!MapView.MapIsEnabled && !hasSingleFired && ((mainTrigger && triggerTimer > BDArmorySettings.TRIGGER_HOLD_TIME) || !mainTrigger)) + { + if (rippleFire) + { + if (Time.time - rippleTimer > 60f / rippleRPM) + { + FireMissile(); + rippleTimer = Time.time; + } + } + else + { + FireMissile(); + hasSingleFired = true; + } + } + } + #endregion Fire #region Weapon Info @@ -2448,7 +2974,7 @@ void DisplaySelectedWeaponMessage() ScreenMessages.RemoveMessage(selectionMessage); selectionMessage.textInstance = null; - selectionText = "Selected Weapon: " + (GetWeaponName(weaponArray[weaponIndex])).ToString(); + selectionText = $"Selected Weapon: {(GetWeaponName(weaponArray[weaponIndex])).ToString()}"; selectionMessage.message = selectionText; selectionMessage.style = ScreenMessageStyle.UPPER_CENTER; @@ -2467,16 +2993,30 @@ string GetWeaponName(IBDWeapon weapon) return weapon.GetShortName(); } } - + float GetWeaponRange(IBDWeapon weapon) + { + if (weapon == null) + { + return -1; + } + else + { + return weapon.GetEngageRange(); + } + } public void UpdateList() { + weaponsListNeedsUpdating = false; weaponTypes.Clear(); + weaponRanges.Clear(); // extension for feature_engagementenvelope: also clear engagement specific weapon lists weaponTypesAir.Clear(); weaponTypesMissile.Clear(); targetMissiles = false; weaponTypesGround.Clear(); weaponTypesSLW.Clear(); + //gunRippleIndex.Clear(); //since there keeps being issues with the more limited ripple dict, lets just make it perisitant for all weapons on the craft + hasAntiRadiationOrdinance = false; if (vessel == null || !vessel.loaded) return; using (var weapon = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) @@ -2491,36 +3031,66 @@ public void UpdateList() if (weap.Current == null) continue; if (weap.Current.GetShortName() == weaponName) { - alreadyAdded = true; + if (weapon.Current.GetWeaponClass() == WeaponClasses.Missile || weapon.Current.GetWeaponClass() == WeaponClasses.Bomb || weapon.Current.GetWeaponClass() == WeaponClasses.SLW) + { + float range = weapon.Current.GetPart().FindModuleImplementing().engageRangeMax; + + if (weaponRanges.TryGetValue(weaponName, out var registeredRanges)) + { + if (registeredRanges.Contains(range)) + alreadyAdded = true; + } + } + else + alreadyAdded = true; //break; } } + if (weapon.Current.GetWeaponClass() == WeaponClasses.Gun || weapon.Current.GetWeaponClass() == WeaponClasses.Rocket || weapon.Current.GetWeaponClass() == WeaponClasses.DefenseLaser) + { + if (!gunRippleIndex.ContainsKey(weapon.Current.GetPartName())) //I think the empty rocketpod? contine might have been tripping up the ripple dict and not adding the hydra + gunRippleIndex.Add(weapon.Current.GetPartName(), 0); + } //dont add empty rocket pods if (weapon.Current.GetWeaponClass() == WeaponClasses.Rocket && - (weapon.Current.GetPart().FindModuleImplementing().rocketPod && !weapon.Current.GetPart().FindModuleImplementing().externalAmmo) && - weapon.Current.GetPart().FindModuleImplementing().GetRocketResource().amount < 1 - && !BDArmorySettings.INFINITE_AMMO) + (weapon.Current.GetPart().FindModuleImplementing().rocketPod && !weapon.Current.GetPart().FindModuleImplementing().externalAmmo) && + weapon.Current.GetPart().FindModuleImplementing().GetRocketResource().amount < 1 + && !BDArmorySettings.INFINITE_AMMO) { continue; } //dont add APS if ((weapon.Current.GetWeaponClass() == WeaponClasses.Gun || weapon.Current.GetWeaponClass() == WeaponClasses.Rocket || weapon.Current.GetWeaponClass() == WeaponClasses.DefenseLaser) && - weapon.Current.GetPart().FindModuleImplementing().isAPS) + (weapon.Current.GetPart().FindModuleImplementing().isAPS && !weapon.Current.GetPart().FindModuleImplementing().dualModeAPS)) { continue; } if (!alreadyAdded) { weaponTypes.Add(weapon.Current); - } + if (weapon.Current.GetWeaponClass() == WeaponClasses.Missile || weapon.Current.GetWeaponClass() == WeaponClasses.Bomb || weapon.Current.GetWeaponClass() == WeaponClasses.SLW) + { + float range = weapon.Current.GetPart().FindModuleImplementing().engageRangeMax; + if (weaponRanges.TryGetValue(weaponName, out var registeredRanges)) + { + registeredRanges.Add(range); + } + else + weaponRanges.Add(weaponName, new List { range }); + } + } EngageableWeapon engageableWeapon = weapon.Current as EngageableWeapon; if (engageableWeapon != null) { if (engageableWeapon.GetEngageAirTargets()) weaponTypesAir.Add(weapon.Current); - if (engageableWeapon.GetEngageMissileTargets()) weaponTypesMissile.Add(weapon.Current); targetMissiles = true; + if (engageableWeapon.GetEngageMissileTargets()) + { + weaponTypesMissile.Add(weapon.Current); + targetMissiles = true; + } if (engageableWeapon.GetEngageGroundTargets()) weaponTypesGround.Add(weapon.Current); if (engageableWeapon.GetEngageSLWTargets()) weaponTypesSLW.Add(weapon.Current); } @@ -2536,7 +3106,15 @@ public void UpdateList() weapon.Current.GetWeaponClass() == WeaponClasses.Missile || weapon.Current.GetWeaponClass() == WeaponClasses.SLW) { + MissileLauncher ml = weapon.Current.GetPart().FindModuleImplementing(); + BDModularGuidance mmg = weapon.Current.GetPart().FindModuleImplementing(); weapon.Current.GetPart().FindModuleImplementing().GetMissileCount(); // #191, Do it this way so the GetMissileCount only updates when missile fired + + if ((ml is not null && ml.TargetingMode == MissileBase.TargetingModes.AntiRad) || (mmg is not null && mmg.TargetingMode == MissileBase.TargetingModes.AntiRad)) + { + hasAntiRadiationOrdinance = true; + antiradTargets = OtherUtils.ParseToFloatArray(ml != null ? ml.antiradTargetTypes : "0,5"); //limited Antirad options for MMG + } } } @@ -2561,12 +3139,10 @@ private void PrepareWeapons() if (vessel == null) return; weaponIndex = Mathf.Clamp(weaponIndex, 0, weaponArray.Length - 1); - if (selectedWeapon == null || selectedWeapon.GetPart() == null || (selectedWeapon.GetPart().vessel != null && selectedWeapon.GetPart().vessel != vessel) || GetWeaponName(selectedWeapon) != GetWeaponName(weaponArray[weaponIndex])) { selectedWeapon = weaponArray[weaponIndex]; - if (vessel.isActiveVessel && Time.time - startTime > 1) { hasSingleFired = true; @@ -2619,9 +3195,18 @@ private void UpdateSelectedWeaponState() if (selectedWeapon != null && (selectedWeapon.GetWeaponClass() == WeaponClasses.Bomb || selectedWeapon.GetWeaponClass() == WeaponClasses.Missile || selectedWeapon.GetWeaponClass() == WeaponClasses.SLW)) { //Debug.Log("[BDArmory.MissileFire]: =====selected weapon: " + selectedWeapon.GetPart().name); - if (!CurrentMissile || CurrentMissile.part.name != selectedWeapon.GetPart().name) + if (!CurrentMissile || CurrentMissile.GetPartName() != selectedWeapon.GetPartName() || CurrentMissile.engageRangeMax != selectedWeaponsEngageRangeMax) { - CurrentMissile = selectedWeapon.GetPart().FindModuleImplementing(); + using (var Missile = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) + while (Missile.MoveNext()) + { + if (Missile.Current == null) continue; + if (Missile.Current.GetPartName() != selectedWeapon.GetPartName()) continue; + if (Missile.Current.launched) continue; + if (Missile.Current.engageRangeMax != selectedWeaponsEngageRangeMax) continue; + CurrentMissile = Missile.Current; + } + //CurrentMissile = selectedWeapon.GetPart().FindModuleImplementing(); } } else @@ -2642,13 +3227,13 @@ private void UpdateSelectedWeaponState() } //gun ripple stuff - if (selectedWeapon != null && (selectedWeapon.GetWeaponClass() == WeaponClasses.Gun || selectedWeapon.GetWeaponClass() == WeaponClasses.Rocket || selectedWeapon.GetWeaponClass() == WeaponClasses.DefenseLaser) && - currentGun.useRippleFire) //currentGun.roundsPerMinute < 1500) + if (selectedWeapon != null && (selectedWeapon.GetWeaponClass() == WeaponClasses.Gun || selectedWeapon.GetWeaponClass() == WeaponClasses.Rocket || selectedWeapon.GetWeaponClass() == WeaponClasses.DefenseLaser)) + //&& currentGun.useRippleFire) //currentGun.roundsPerMinute < 1500) { float counter = 0; // Used to get a count of the ripple weapons. a float version of rippleGunCount. - gunRippleIndex = 0; + //gunRippleIndex.Clear(); // This value will be incremented as we set the ripple weapons - rippleGunCount = 0; + rippleGunCount.Clear(); float weaponRpm = 0; // used to set the rippleGunRPM // JDK: this looks like it can be greatly simplified... @@ -2736,21 +3321,29 @@ private void UpdateSelectedWeaponState() rippleWeapons.Add(weapCnt.Current); counter += weaponRpm; // grab sum of weapons rpm } - gunRippleRpm = counter; - //number of seconds between each gun firing; will reduce with increasing RPM or number of guns - float timeDelayPerGun = 60f / gunRippleRpm; // rpm*counter will return the square of rpm now - // Now lets act on the filtered list. + + //ripple for non-homogeneous groups needs to be setup per guntype, else a slow cannon will have the same firedelay as a fast MG using (List.Enumerator weapon = rippleWeapons.GetEnumerator()) while (weapon.MoveNext()) { + int GunCount = 0; if (weapon.Current == null) continue; - // set the weapon ripple index just before we increment rippleGunCount. - weapon.Current.rippleIndex = rippleGunCount; - //set the time delay for moving to next index - weapon.Current.initialFireDelay = timeDelayPerGun; weapon.Current.useRippleFire = ro.rippleFire; - rippleGunCount++; + if (!rippleGunCount.ContainsKey(weapon.Current.WeaponName)) //don't setup copies of a guntype if we've already done that + { + for (int w = 0; w < rippleWeapons.Count; w++) + { + if (weapon.Current.WeaponName == rippleWeapons[w].WeaponName) + { + rippleWeapons[w].rippleIndex = GunCount; //this will mean that a group of two+ different RPM guns will start firing at the same time, then each subgroup will independantly ripple + GunCount++; + } + } + rippleGunCount.Add(weapon.Current.WeaponName, GunCount); + } + weapon.Current.initialFireDelay = 60 / (weapon.Current.roundsPerMinute * rippleGunCount[weapon.Current.WeaponName]); + //Debug.Log("[RIPPLEDEBUG]" + weapon.Current.WeaponName + " rippleIndex: " + weapon.Current.rippleIndex + "; initialfiredelay: " + weapon.Current.initialFireDelay); } } @@ -2982,7 +3575,7 @@ void SetRotaryRails() return; } - if (rotRail.Current.readyMissile.part.name != cm.part.name) + if (rotRail.Current.readyMissile.GetPartName() != cm.GetPartName()) { rotRail.Current.RotateToMissile(cm); } @@ -2993,7 +3586,7 @@ void SetRotaryRails() { rotRail.Current.RotateToMissile(cm); } - else if (rotRail.Current.nextMissile.part.name != cm.part.name) + else if (rotRail.Current.nextMissile.GetPartName() != cm.GetPartName()) { rotRail.Current.RotateToMissile(cm); } @@ -3027,7 +3620,7 @@ void SetDeployableRails() { if (mt.Current == null) continue; if (!mt.Current.isActiveAndEnabled) continue; - if (weaponIndex > 0 && cm && mt.Current.ContainsMissileOfType(cm) && cm.deployableRail == mt.Current) + if (weaponIndex > 0 && cm && mt.Current.ContainsMissileOfType(cm) && cm.deployableRail == mt.Current && !cm.launched) { mt.Current.EnableRail(); } @@ -3064,7 +3657,6 @@ public void CycleWeapon(int index) index = 0; } weaponIndex = index; - UpdateList(); if (vessel.isActiveVessel && !guardMode) @@ -3105,7 +3697,9 @@ private MissileBase GetAsymMissile() MissileLauncher launcher = ml.Current as MissileLauncher; if (launcher != null) { - if (weaponArray[weaponIndex].GetPart() == null || launcher.part.name != weaponArray[weaponIndex].GetPart().name) continue; + if (weaponArray[weaponIndex].GetPart() == null || launcher.GetPartName() != weaponArray[weaponIndex].GetPartName()) continue; + if (launcher.launched) continue; + if (launcher.engageRangeMax != selectedWeaponsEngageRangeMax) continue; } else { @@ -3137,13 +3731,13 @@ private MissileBase GetRotaryReadyMissile() //TODO BDModularGuidance, ModuleDrone: Implemente rotaryRail support MissileLauncher missile = CurrentMissile as MissileLauncher; if (missile == null) return null; - if (weaponArray[weaponIndex].GetPart() != null && missile.part.name == weaponArray[weaponIndex].GetPart().name) + if (weaponArray[weaponIndex].GetPart() != null && missile.GetPartName() == weaponArray[weaponIndex].GetPartName()) { if (!missile.rotaryRail) { return missile; } - if (missile.rotaryRail.readyToFire && missile.rotaryRail.readyMissile == CurrentMissile) + if (missile.rotaryRail.readyToFire && missile.rotaryRail.readyMissile == CurrentMissile && !missile.launched) { return missile; } @@ -3152,14 +3746,14 @@ private MissileBase GetRotaryReadyMissile() while (ml.MoveNext()) { if (ml.Current == null) continue; - if (weaponArray[weaponIndex].GetPart() == null || ml.Current.part.name != weaponArray[weaponIndex].GetPart().name) continue; - + if (weaponArray[weaponIndex].GetPart() == null || ml.Current.GetPartName() != weaponArray[weaponIndex].GetPartName()) continue; + if (ml.Current.launched) continue; if (!ml.Current.rotaryRail) { return ml.Current; } if (ml.Current.rotaryRail.readyMissile == null || ml.Current.rotaryRail.readyMissile.part == null) continue; - if (ml.Current.rotaryRail.readyToFire && ml.Current.rotaryRail.readyMissile.part.name == weaponArray[weaponIndex].GetPart().name) + if (ml.Current.rotaryRail.readyToFire && ml.Current.rotaryRail.readyMissile.GetPartName() == weaponArray[weaponIndex].GetPartName()) { return ml.Current.rotaryRail.readyMissile; } @@ -3180,6 +3774,7 @@ bool CheckBombClearance(MissileBase ml) //TODO BDModularGuidance: Bombs and turrents MissileLauncher launcher = ml as MissileLauncher; + Transform referenceTransform = launcher.multiLauncher.overrideReferenceTransform ? launcher.part.FindModelTransform(launcher.multiLauncher.launchTransformName).GetChild(0) : launcher.MissileReferenceTransform; if (launcher != null) { if (launcher.rotaryRail && launcher.rotaryRail.readyMissile != ml) @@ -3192,38 +3787,27 @@ bool CheckBombClearance(MissileBase ml) return false; } - int layerMask = (int)(LayerMasks.Parts | LayerMasks.Scenery | LayerMasks.Unknown19); + const int layerMask = (int)(LayerMasks.Parts | LayerMasks.Scenery | LayerMasks.Unknown19 | LayerMasks.Wheels); if (ml.dropTime >= 0.1f) { //debug lines - LineRenderer lr = null; - if (BDArmorySettings.DRAW_DEBUG_LINES && BDArmorySettings.DRAW_AIMERS) + if (BDArmorySettings.DEBUG_LINES && BDArmorySettings.DRAW_AIMERS) { lr = GetComponent(); - if (!lr) - { - lr = gameObject.AddComponent(); - } + if (!lr) { lr = gameObject.AddComponent(); } lr.enabled = true; lr.startWidth = .1f; lr.endWidth = .1f; } - else - { - if (gameObject.GetComponent()) - { - gameObject.GetComponent().enabled = false; - } - } float radius = launcher.decoupleForward ? launcher.ClearanceRadius : launcher.ClearanceLength; float time = Mathf.Min(ml.dropTime, 2f); Vector3 direction = ((launcher.decoupleForward - ? ml.MissileReferenceTransform.transform.forward - : -ml.MissileReferenceTransform.transform.up) * launcher.decoupleSpeed * time) + + ? referenceTransform.forward + : -referenceTransform.up) * launcher.decoupleSpeed * time) + ((FlightGlobals.getGeeForceAtPosition(transform.position) - vessel.acceleration) * 0.5f * time * time); - Vector3 crossAxis = Vector3.Cross(direction, ml.MissileReferenceTransform.transform.right).normalized; + Vector3 crossAxis = Vector3.Cross(direction, referenceTransform.transform.right).normalized; float rayDistance; if (launcher.thrust == 0 || launcher.cruiseThrust == 0) @@ -3238,12 +3822,12 @@ bool CheckBombClearance(MissileBase ml) Ray[] rays = { - new Ray(ml.MissileReferenceTransform.position - (radius*crossAxis), direction), - new Ray(ml.MissileReferenceTransform.position + (radius*crossAxis), direction), - new Ray(ml.MissileReferenceTransform.position, direction) + new Ray(referenceTransform.position - (radius*crossAxis), direction), + new Ray(referenceTransform.position + (radius*crossAxis), direction), + new Ray(referenceTransform.position, direction) }; - if (lr) + if (lr != null && lr.enabled) { lr.useWorldSpace = false; lr.positionCount = 4; @@ -3256,54 +3840,82 @@ bool CheckBombClearance(MissileBase ml) using (IEnumerator rt = rays.AsEnumerable().GetEnumerator()) while (rt.MoveNext()) { - RaycastHit[] hits = Physics.RaycastAll(rt.Current, rayDistance, layerMask); - using (IEnumerator t1 = hits.AsEnumerable().GetEnumerator()) - while (t1.MoveNext()) + var hitCount = Physics.RaycastNonAlloc(rt.Current, clearanceHits, rayDistance, layerMask); + if (hitCount == clearanceHits.Length) // If there's a whole bunch of stuff in the way (unlikely), then we need to increase the size of our hits buffer. + { + clearanceHits = Physics.RaycastAll(rt.Current, rayDistance, layerMask); + hitCount = clearanceHits.Length; + } + using (var t = clearanceHits.Take(hitCount).GetEnumerator()) + while (t.MoveNext()) { - Part p = t1.Current.collider.GetComponentInParent(); + Part p = t.Current.collider.GetComponentInParent(); if ((p == null || p == ml.part) && p != null) continue; - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log("[BDArmory.MissileFire]: RAYCAST HIT, clearance is FALSE! part=" + (p != null ? p.name : null) + ", collider=" + (p != null ? p.collider : null)); + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log($"[BDArmory.MissileFire]: RAYCAST HIT, clearance is FALSE! part={(p != null ? p.name : null)}, collider+{(p != null ? p.collider : null)}"); return false; } } return true; } - //forward check for no-drop missiles - RaycastHit[] hitparts = Physics.RaycastAll(new Ray(ml.MissileReferenceTransform.position, ml.GetForwardTransform()), 50, layerMask); - using (IEnumerator t = hitparts.AsEnumerable().GetEnumerator()) - while (t.MoveNext()) + { //forward check for no-drop missiles + var ray = new Ray(ml.MissileReferenceTransform.position, ml.GetForwardTransform()); + var hitCount = Physics.RaycastNonAlloc(ray, clearanceHits, 50, layerMask); + if (hitCount == clearanceHits.Length) // If there's a whole bunch of stuff in the way (unlikely), then we need to increase the size of our hits buffer. { - Part p = t.Current.collider.GetComponentInParent(); - if ((p == null || p == ml.part) && p != null) continue; - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log("[BDArmory.MissileFire]: RAYCAST HIT, clearance is FALSE! part=" + (p != null ? p.name : null) + ", collider=" + (p != null ? p.collider : null)); - return false; + clearanceHits = Physics.RaycastAll(ray, 50, layerMask); + hitCount = clearanceHits.Length; } + using (var t = clearanceHits.Take(hitCount).GetEnumerator()) + while (t.MoveNext()) + { + Part p = t.Current.collider.GetComponentInParent(); + if ((p == null || p == ml.part) && p != null) continue; + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log($"[BDArmory.MissileFire]: RAYCAST HIT, clearance is FALSE! part={(p != null ? p.name : null)}, collider={(p != null ? p.collider : null)}"); + return false; + } + } } return true; } void RefreshModules() { + modulesNeedRefreshing = false; + cmPrioritiesNeedRefreshing = true; VesselModuleRegistry.OnVesselModified(vessel); // Make sure the registry is up-to-date. - radars = VesselModuleRegistry.GetModules(vessel); - // DISABLE RADARS - /* - List.Enumerator rad = radars.GetEnumerator(); - while (rad.MoveNext()) + _radars = VesselModuleRegistry.GetModules(vessel); + if (_radars != null) { - if (rad.Current == null) continue; - rad.Current.EnsureVesselRadarData(); - if (rad.Current.radarEnabled) rad.Current.EnableRadar(); + // DISABLE RADARS + /* + List.Enumerator rad = _radars.GetEnumerator(); + while (rad.MoveNext()) + { + if (rad.Current == null) continue; + rad.Current.EnsureVesselRadarData(); + if (rad.Current.radarEnabled) rad.Current.EnableRadar(); + } + rad.Dispose(); + */ + MaxradarLocks = 0; + using (List.Enumerator rd = _radars.GetEnumerator()) + while (rd.MoveNext()) + { + if (rd.Current != null && rd.Current.canLock) + { + if (rd.Current.maxLocks > 0) MaxradarLocks += rd.Current.maxLocks; + } + } } - rad.Dispose(); - */ - jammers = VesselModuleRegistry.GetModules(vessel); - targetingPods = VesselModuleRegistry.GetModules(vessel); - wmModules = VesselModuleRegistry.GetModules(vessel); + _irsts = VesselModuleRegistry.GetModules(vessel); + _jammers = VesselModuleRegistry.GetModules(vessel); + _cloaks = VesselModuleRegistry.GetModules(vessel); + _targetingPods = VesselModuleRegistry.GetModules(vessel); + _wmModules = VesselModuleRegistry.GetModules(vessel); } #endregion Weapon Info @@ -3317,9 +3929,57 @@ void SmartFindTarget() var lastTarget = currentTarget; List targetsTried = new List(); string targetDebugText = ""; - targetsAssigned.Clear(); //fixes fixed guns not firing if Multitargeting >1 missilesAssigned.Clear(); + if (multiMissileTgtNum > 1 && BDATargetManager.TargetList(Team).Count > 1) + { + if (CurrentMissile || PreviousMissile) //if there are multiple potential targets, see how many can be fired at with missiles + { + if (firedMissiles >= maxMissilesOnTarget) + { + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.MissileFire]: max missiles on target; switching to new target!"); + if (Vector3.Distance(transform.position + vessel.Velocity(), currentTarget.position + currentTarget.velocity) < gunRange * 0.75f) //don't swap away from current target if about to enter gunrange + { + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.MissileFire]: max targets fired on, but about to enter Gun range; keeping current target"); + return; + } + if (PreviousMissile) + { + if (PreviousMissile.TargetingMode == MissileBase.TargetingModes.Laser) //don't switch from current target if using LASMs to keep current target painted + { + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.MissileFire]: max targets fired on with LASMs, keeping target painted!"); + if (currentTarget != null) return; //don't paint a destroyed target + } + if (!(PreviousMissile.TargetingMode == MissileBase.TargetingModes.Radar && !PreviousMissile.radarLOAL)) + { + //if (vesselRadarData != null) vesselRadarData.UnlockCurrentTarget();//unlock current target only if missile isn't slaved to ship radar guidance to allow new F&F lock + //enabling this has the radar blip off after firing missile, having it on requires waiting 2 sec for the radar do decide it needs to swap to another target, but will continue to guide current missile (assuming sufficient radar FOV) + } + } + heatTarget = TargetSignatureData.noTarget; //clear holdover targets when switching targets + antiRadiationTarget = Vector3.zero; + } + } + using (List.Enumerator target = BDATargetManager.TargetList(Team).GetEnumerator()) + { + while (target.MoveNext()) + { + if (missilesAway.ContainsKey(target.Current)) + { + if (missilesAway[target.Current] >= maxMissilesOnTarget) + { + targetsAssigned.Add(target.Current); + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileFire]: Adding {target.Current.Vessel.GetName()} to exclusion list; length: {targetsAssigned.Count}"); + } + } + } + } + if (targetsAssigned.Count == BDATargetManager.TargetList(Team).Count) //oops, already fired missiles at all available targets + { + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.MissileFire]: max targets fired on, resetting target list!"); + targetsAssigned.Clear(); //clear targets tried, so AI can track best current target until such time as it can fire again + } + } if (overrideTarget) //begin by checking the override target, since that takes priority { @@ -3327,16 +3987,16 @@ void SmartFindTarget() SetTarget(overrideTarget); if (SmartPickWeapon_EngagementEnvelope(overrideTarget)) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_AI) { - Debug.Log("[BDArmory.MissileFire]: " + vessel.vesselName + " is engaging an override target with " + selectedWeapon); + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} is engaging an override target with {selectedWeapon}"); } overrideTimer = 15f; return; } - else if (BDArmorySettings.DRAW_DEBUG_LABELS) + else if (BDArmorySettings.DEBUG_AI) { - Debug.Log("[BDArmory.MissileFire]: " + vessel.vesselName + " is engaging an override target with failed to engage its override target!"); + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} is engaging an override target with failed to engage its override target!"); } } overrideTarget = null; //null the override target if it cannot be used @@ -3353,9 +4013,9 @@ void SmartFindTarget() SetTarget(potentialTarget); if (SmartPickWeapon_EngagementEnvelope(potentialTarget)) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_AI) { - Debug.Log("[BDArmory.MissileFire]: " + vessel.vesselName + " is engaging incoming missile (" + potentialTarget.Vessel.vesselName + ") with " + selectedWeapon); + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} is engaging incoming missile ({potentialTarget.Vessel.GetName()}:{potentialTarget.Vessel.parts[0].persistentId}) with {selectedWeapon}"); } return; } @@ -3369,9 +4029,9 @@ void SmartFindTarget() SetTarget(potentialTarget); if (SmartPickWeapon_EngagementEnvelope(potentialTarget)) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_AI) { - Debug.Log("[BDArmory.MissileFire]: " + vessel.vesselName + " is engaging unengaged missile (" + potentialTarget.Vessel.vesselName + ") with " + selectedWeapon); + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} is engaging unengaged missile ({potentialTarget.Vessel.GetName()}:{potentialTarget.Vessel.parts[0].persistentId}) with {selectedWeapon}"); } return; } @@ -3379,77 +4039,33 @@ void SmartFindTarget() } //=========END HIGH PRIORITY MISSILES============= - //if AIRBORNE, try to engage airborne target - if (!vessel.LandedOrSplashed) - { - TargetInfo potentialAirTarget = null; - - if (BDArmorySettings.DEFAULT_FFA_TARGETING) - { - potentialAirTarget = BDATargetManager.GetClosestTargetWithBiasAndHysteresis(this); - targetDebugText = " is engaging an airborne target in FFA with "; - } - else if (this.targetPriorityEnabled) - { - potentialAirTarget = BDATargetManager.GetHighestPriorityTarget(this); - targetDebugText = " is engaging highest priority airborne target with "; - } - else - { - if (pilotAI && pilotAI.IsExtending) - { - potentialAirTarget = BDATargetManager.GetAirToAirTargetAbortExtend(this, 1500, 0.2f); - targetDebugText = " is aborting extend and engaging an incoming airborne target with "; - } - else - { - potentialAirTarget = BDATargetManager.GetAirToAirTarget(this); - targetDebugText = " is engaging an airborne target with "; - } - } - - if (potentialAirTarget) - { - targetsTried.Add(potentialAirTarget); - SetTarget(potentialAirTarget); - // Pick target if we have a viable weapon or target priority/FFA targeting is in use - // || targetPriorityEnabled || BDArmorySettings.DEFAULT_FFA_TARGETING - if (SmartPickWeapon_EngagementEnvelope(potentialAirTarget) && HasWeaponsAndAmmo()) - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - Debug.Log("[BDArmory.MissileFire]: " + vessel.vesselName + targetDebugText + selectedWeapon); - } - return; - } - else if (!BDArmorySettings.DISABLE_RAMMING) - { - if (!HasWeaponsAndAmmo() && pilotAI != null && pilotAI.allowRamming) - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - Debug.Log("[BDArmory.MissileFire]: " + vessel.vesselName + targetDebugText + "ramming."); - } - return; - } - } - } - } - //============VESSEL THREATS============ // select target based on competition style if (BDArmorySettings.DEFAULT_FFA_TARGETING) { potentialTarget = BDATargetManager.GetClosestTargetWithBiasAndHysteresis(this); - targetDebugText = " is engaging an FFA target with "; + targetDebugText = " is engaging a FFA target with "; } else if (this.targetPriorityEnabled) { potentialTarget = BDATargetManager.GetHighestPriorityTarget(this); - targetDebugText = " is engaging highest priority target (" + (potentialTarget != null ? potentialTarget.Vessel.vesselName : "null") + ") with "; + targetDebugText = $" is engaging highest priority target ({(potentialTarget != null ? potentialTarget.Vessel.vesselName : "null")}) with "; } else { + if (!vessel.LandedOrSplashed) + { + if (pilotAI && pilotAI.IsExtending) + { + potentialTarget = BDATargetManager.GetAirToAirTargetAbortExtend(this, 1500, 0.2f); + targetDebugText = " is aborting extend and engaging an incoming airborne target with "; + } + else + { + potentialTarget = BDATargetManager.GetAirToAirTarget(this); + targetDebugText = " is engaging an airborne target with "; + } + } potentialTarget = BDATargetManager.GetLeastEngagedTarget(this); targetDebugText = " is engaging the least engaged target with "; } @@ -3458,26 +4074,31 @@ void SmartFindTarget() { targetsTried.Add(potentialTarget); SetTarget(potentialTarget); - /* - if (CrossCheckWithRWR(potentialTarget) && TryPickAntiRad(potentialTarget)) + + // Pick target if we have a viable weapon or target priority/FFA targeting is in use + if ((SmartPickWeapon_EngagementEnvelope(potentialTarget) || this.targetPriorityEnabled || BDArmorySettings.DEFAULT_FFA_TARGETING) && HasWeaponsAndAmmo()) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_AI) { - Debug.Log("[BDArmory.MissileFire]: " + vessel.vesselName + " is engaging the least engaged radar target with " + - selectedWeapon.GetShortName()); + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName + targetDebugText + (selectedWeapon != null ? selectedWeapon.GetShortName() : "")}"); } + //need to check that target is actually being seen, and not just being recalled due to object permanence + //if (CanSeeTarget(potentialTarget, false)) + //{ + // BDATargetManager.ReportVessel(potentialTarget.Vessel, this); //have it so AI can see and register a target (no radar + FoV angle < 360, radar turns off due to incoming HARM, etc) + //} //target would already be listed as seen/radar detected via GuardScan/Radar; all CanSee does is check if the detected time is < 30s return; } - */ - - // Pick target if we have a viable weapon or target priority/FFA targeting is in use - if (SmartPickWeapon_EngagementEnvelope(potentialTarget) || this.targetPriorityEnabled || BDArmorySettings.DEFAULT_FFA_TARGETING) + else if (!BDArmorySettings.DISABLE_RAMMING) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (!HasWeaponsAndAmmo() && pilotAI != null && pilotAI.allowRamming) { - Debug.Log("[BDArmory.MissileFire]: " + vessel.vesselName + targetDebugText + (selectedWeapon != null ? selectedWeapon.GetShortName() : "")); + if (BDArmorySettings.DEBUG_AI) + { + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName + targetDebugText} ramming."); + } + return; } - return; } } @@ -3490,7 +4111,7 @@ void SmartFindTarget() /* if (CrossCheckWithRWR(potentialTarget) && TryPickAntiRad(potentialTarget)) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_LABELS) { Debug.Log("[BDArmory.MissileFire]: " + vessel.vesselName + " is engaging the closest radar target with " + selectedWeapon.GetShortName()); @@ -3500,9 +4121,9 @@ void SmartFindTarget() */ if (SmartPickWeapon_EngagementEnvelope(potentialTarget)) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_AI) { - Debug.Log("[BDArmory.MissileFire]: " + vessel.vesselName + " is engaging the closest target (" + potentialTarget.Vessel.vesselName + ") with " + selectedWeapon.GetShortName()); + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} is engaging the closest target ({potentialTarget.Vessel.vesselName}) with {selectedWeapon.GetShortName()}"); } return; } @@ -3520,9 +4141,9 @@ void SmartFindTarget() SetTarget(potentialTarget); if (SmartPickWeapon_EngagementEnvelope(potentialTarget)) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_AI) { - Debug.Log("[BDArmory.MissileFire]: " + vessel.vesselName + " is engaging the least engaged missile (" + potentialTarget.Vessel.vesselName + ") with " + selectedWeapon.GetShortName()); + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} is engaging the least engaged missile ({potentialTarget.Vessel.vesselName}) with {selectedWeapon.GetShortName()}"); } return; } @@ -3536,9 +4157,9 @@ void SmartFindTarget() SetTarget(potentialTarget); if (SmartPickWeapon_EngagementEnvelope(potentialTarget)) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_AI) { - Debug.Log("[BDArmory.MissileFire]: " + vessel.vesselName + " is engaging the closest hostile missile (" + potentialTarget.Vessel.vesselName + ") with " + selectedWeapon.GetShortName()); + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} is engaging the closest hostile missile ({potentialTarget.Vessel.vesselName}) with {selectedWeapon.GetShortName()}"); } return; } @@ -3553,10 +4174,9 @@ void SmartFindTarget() if (finalTargets.Current == null) continue; SetTarget(finalTargets.Current); if (!SmartPickWeapon_EngagementEnvelope(finalTargets.Current)) continue; - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_AI) { - Debug.Log("[BDArmory.MissileFire]: " + vessel.vesselName + " is engaging a final target with " + - selectedWeapon.GetShortName()); + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} is engaging a final target with {selectedWeapon.GetShortName()}"); } return; } @@ -3564,13 +4184,14 @@ void SmartFindTarget() //no valid targets found if (potentialTarget == null || selectedWeapon == null) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_AI) { - Debug.Log("[BDArmory.MissileFire]: " + vessel.vesselName + " is disengaging - no valid weapons - no valid targets"); + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} is disengaging - no valid weapons - no valid targets"); } CycleWeapon(0); SetTarget(null); - if (vesselRadarData && vesselRadarData.locked && missilesAway == 0) // Don't unlock targets while we've got missiles in the air. + + if (vesselRadarData && vesselRadarData.locked && missilesAway.Count == 0) // Don't unlock targets while we've got missiles in the air. { vesselRadarData.UnlockAllTargets(); } @@ -3599,37 +4220,42 @@ void SmartFindSecondaryTargets() //1. incoming missile threats //2. highest priority non-targeted target //3. closest non-targeted target - - for (int i = 0; i < multiTargetNum; i++) + if (targetMissiles) { - TargetInfo potentialMissileTarget = null; - //=========MISSILES============= - //prioritize incoming missiles - potentialMissileTarget = BDATargetManager.GetMissileTarget(this, true); - if (potentialMissileTarget) - { - missilesAssigned.Add(potentialMissileTarget); - targetsTried.Add(potentialMissileTarget); - return; - } - //then provide point defense umbrella - potentialMissileTarget = BDATargetManager.GetClosestMissileTarget(this); - if (potentialMissileTarget) - { - missilesAssigned.Add(potentialMissileTarget); - targetsTried.Add(potentialMissileTarget); - return; - } - potentialMissileTarget = BDATargetManager.GetUnengagedMissileTarget(this); - if (potentialMissileTarget) + for (int i = 0; i < Math.Max(multiTargetNum, multiMissileTgtNum) - 1; i++) { - missilesAssigned.Add(potentialMissileTarget); - targetsTried.Add(potentialMissileTarget); - return; + TargetInfo potentialMissileTarget = null; + //=========MISSILES============= + //prioritize incoming missiles + potentialMissileTarget = BDATargetManager.GetMissileTarget(this, true); + if (potentialMissileTarget) + { + missilesAssigned.Add(potentialMissileTarget); + targetsTried.Add(potentialMissileTarget); + if (BDArmorySettings.DEBUG_AI) + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} targeting missile {potentialMissileTarget.Vessel.GetName()}:{potentialMissileTarget.Vessel.parts[0].persistentId} as a secondary target"); + } + //then provide point defense umbrella + potentialMissileTarget = BDATargetManager.GetClosestMissileTarget(this); + if (potentialMissileTarget) + { + missilesAssigned.Add(potentialMissileTarget); + targetsTried.Add(potentialMissileTarget); + if (BDArmorySettings.DEBUG_AI) + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} targeting closest missile {potentialMissileTarget.Vessel.GetName()}:{potentialMissileTarget.Vessel.parts[0].persistentId} as a secondary target"); + } + potentialMissileTarget = BDATargetManager.GetUnengagedMissileTarget(this); + if (potentialMissileTarget) + { + missilesAssigned.Add(potentialMissileTarget); + targetsTried.Add(potentialMissileTarget); + if (BDArmorySettings.DEBUG_AI) + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} targeting free missile {potentialMissileTarget.Vessel.GetName()}:{potentialMissileTarget.Vessel.parts[0].persistentId} as a secondary target"); + } } } - for (int i = 0; i < multiTargetNum; i++) + for (int i = 0; i < Math.Max(multiTargetNum, multiMissileTgtNum) - 1; i++) //primary target already added, so subtract 1 from nultitargetnum { TargetInfo potentialTarget = null; //============VESSEL THREATS============ @@ -3641,7 +4267,8 @@ void SmartFindSecondaryTargets() { targetsAssigned.Add(potentialTarget); targetsTried.Add(potentialTarget); - return; + if (BDArmorySettings.DEBUG_AI) + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} targeting priority target {potentialTarget.Vessel.GetName()} as a secondary target"); } potentialTarget = BDATargetManager.GetClosestTarget(this); if (BDArmorySettings.DEFAULT_FFA_TARGETING) @@ -3652,7 +4279,8 @@ void SmartFindSecondaryTargets() { targetsAssigned.Add(potentialTarget); targetsTried.Add(potentialTarget); - return; + if (BDArmorySettings.DEBUG_AI) + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} targeting bias target {potentialTarget.Vessel.GetName()} as a secondary target"); } } using (List.Enumerator finalTargets = BDATargetManager.GetAllTargetsExcluding(targetsTried, this).GetEnumerator()) @@ -3660,20 +4288,23 @@ void SmartFindSecondaryTargets() { if (finalTargets.Current == null) continue; targetsAssigned.Add(finalTargets.Current); - return; + targetsTried.Add(finalTargets.Current); + if (BDArmorySettings.DEBUG_AI) + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} targeting remaining target {finalTargets.Current.Vessel.GetName()} as a secondary target"); } - //else - if (potentialTarget == null) - { - return; - } } - Debug.Log("[BDArmory.MissileFire]: Unhandled secondary target case"); + if (targetsAssigned.Count == 0) + { + if (BDArmorySettings.DEBUG_AI) + Debug.Log("[BDArmory.MissileFire]: No available secondary targets"); + } } // Update target priority UI public void UpdateTargetPriorityUI(TargetInfo target) { + // Return if the UI isn't visible + if (part.PartActionWindow == null || !part.PartActionWindow.isActiveAndEnabled) return; // Return if no target if (target == null) { @@ -3685,12 +4316,14 @@ public void UpdateTargetPriorityUI(TargetInfo target) // Get UI fields var TargetBiasFields = Fields["targetBias"]; var TargetRangeFields = Fields["targetWeightRange"]; + var TargetPreferenceFields = Fields["targetWeightAirPreference"]; var TargetATAFields = Fields["targetWeightATA"]; var TargetAoDFields = Fields["targetWeightAoD"]; var TargetAccelFields = Fields["targetWeightAccel"]; var TargetClosureTimeFields = Fields["targetWeightClosureTime"]; var TargetWeaponNumberFields = Fields["targetWeightWeaponNumber"]; var TargetMassFields = Fields["targetWeightMass"]; + var TargetDamageFields = Fields["targetWeightDamage"]; var TargetFriendliesEngagingFields = Fields["targetWeightFriendliesEngaging"]; var TargetThreatFields = Fields["targetWeightThreat"]; var TargetProtectTeammateFields = Fields["targetWeightProtectTeammate"]; @@ -3700,12 +4333,14 @@ public void UpdateTargetPriorityUI(TargetInfo target) // Calculate score values float targetBiasValue = targetBias; float targetRangeValue = target.TargetPriRange(this); + float targetPreferencevalue = target.TargetPriEngagement(target.weaponManager); float targetATAValue = target.TargetPriATA(this); float targetAoDValue = target.TargetPriAoD(this); float targetAccelValue = target.TargetPriAcceleration(); float targetClosureTimeValue = target.TargetPriClosureTime(this); float targetWeaponNumberValue = target.TargetPriWeapons(target.weaponManager, this); float targetMassValue = target.TargetPriMass(target.weaponManager, this); + float targetDamageValue = target.TargetPriDmg(target.weaponManager); float targetFriendliesEngagingValue = target.TargetPriFriendliesEngaging(this); float targetThreatValue = target.TargetPriThreat(target.weaponManager, this); float targetProtectTeammateValue = target.TargetPriProtectTeammate(target.weaponManager, this); @@ -3715,11 +4350,13 @@ public void UpdateTargetPriorityUI(TargetInfo target) // Calculate total target score float targetScore = targetBiasValue * ( targetWeightRange * targetRangeValue + + targetWeightAirPreference * targetPreferencevalue + targetWeightATA * targetATAValue + targetWeightAccel * targetAccelValue + targetWeightClosureTime * targetClosureTimeValue + targetWeightWeaponNumber * targetWeaponNumberValue + targetWeightMass * targetMassValue + + targetWeightDamage * targetDamageValue + targetWeightFriendliesEngaging * targetFriendliesEngagingValue + targetWeightThreat * targetThreatValue + targetWeightAoD * targetAoDValue + @@ -3728,19 +4365,21 @@ public void UpdateTargetPriorityUI(TargetInfo target) targetWeightAttackVIP * targetAttackVIPValue); // Update GUI - TargetBiasFields.guiName = targetBiasLabel + ": " + targetBiasValue.ToString("0.00"); - TargetRangeFields.guiName = targetRangeLabel + ": " + targetRangeValue.ToString("0.00"); - TargetATAFields.guiName = targetATALabel + ": " + targetATAValue.ToString("0.00"); - TargetAoDFields.guiName = targetAoDLabel + ": " + targetAoDValue.ToString("0.00"); - TargetAccelFields.guiName = targetAccelLabel + ": " + targetAccelValue.ToString("0.00"); - TargetClosureTimeFields.guiName = targetClosureTimeLabel + ": " + targetClosureTimeValue.ToString("0.00"); - TargetWeaponNumberFields.guiName = targetWeaponNumberLabel + ": " + targetWeaponNumberValue.ToString("0.00"); - TargetMassFields.guiName = targetMassLabel + ": " + targetMassValue.ToString("0.00"); - TargetFriendliesEngagingFields.guiName = targetFriendliesEngagingLabel + ": " + targetFriendliesEngagingValue.ToString("0.00"); - TargetThreatFields.guiName = targetThreatLabel + ": " + targetThreatValue.ToString("0.00"); - TargetProtectTeammateFields.guiName = targetProtectTeammateLabel + ": " + targetProtectTeammateValue.ToString("0.00"); - TargetProtectVIPFields.guiName = targetProtectVIPLabel + ": " + targetProtectVIPValue.ToString("0.00"); - TargetAttackVIPFields.guiName = targetAttackVIPLabel + ": " + targetAttackVIPValue.ToString("0.00"); + TargetBiasFields.guiName = targetBiasLabel + $": {targetBiasValue:0.00}"; + TargetRangeFields.guiName = targetRangeLabel + $": {targetRangeValue:0.00}"; + TargetPreferenceFields.guiName = targetPreferenceLabel + $": {targetPreferencevalue:0.00}"; + TargetATAFields.guiName = targetATALabel + $": {targetATAValue:0.00}"; + TargetAoDFields.guiName = targetAoDLabel + $": {targetAoDValue:0.00}"; + TargetAccelFields.guiName = targetAccelLabel + $": {targetAccelValue:0.00}"; + TargetClosureTimeFields.guiName = targetClosureTimeLabel + $": {targetClosureTimeValue:0.00}"; + TargetWeaponNumberFields.guiName = targetWeaponNumberLabel + $": {targetWeaponNumberValue:0.00}"; + TargetMassFields.guiName = targetMassLabel + $": {targetMassValue:0.00}"; + TargetDamageFields.guiName = targetDmgLabel + $": {targetDamageValue:0.00}"; + TargetFriendliesEngagingFields.guiName = targetFriendliesEngagingLabel + $": {targetFriendliesEngagingValue:0.00}"; + TargetThreatFields.guiName = targetThreatLabel + $": {targetThreatValue:0.00}"; + TargetProtectTeammateFields.guiName = targetProtectTeammateLabel + $": {targetProtectTeammateValue:0.00}"; + TargetProtectVIPFields.guiName = targetProtectVIPLabel + $": {targetProtectVIPValue:0.00}"; + TargetAttackVIPFields.guiName = targetAttackVIPLabel + $": {targetAttackVIPValue:0.00}"; TargetScoreLabel = targetScore.ToString("0.00"); TargetLabel = target.Vessel.GetDisplayName(); @@ -3821,6 +4460,7 @@ bool SmartPickWeapon_EngagementEnvelope(TargetInfo target) float candidateYTraverse = Gun.yawRange; float candidatePTraverse = Gun.maxPitch; float candidateMinrange = Gun.engageRangeMin; + float candidateMaxRange = Gun.engageRangeMax; bool candidatePFuzed = Gun.eFuzeType == ModuleWeapon.FuzeTypes.Proximity || Gun.eFuzeType == ModuleWeapon.FuzeTypes.Flak; bool candidateVTFuzed = Gun.eFuzeType == ModuleWeapon.FuzeTypes.Timed || Gun.eFuzeType == ModuleWeapon.FuzeTypes.Flak; float Cannistershot = Gun.ProjectileCount; @@ -3844,7 +4484,7 @@ bool SmartPickWeapon_EngagementEnvelope(TargetInfo target) { candidateRPM *= (1 + ((Cannistershot / 2) / 100)); // weight selection towards cluster ammo based on submunition count } - if (candidateMinrange > distance) + if (candidateMinrange > distance || distance > candidateMaxRange) { candidateRPM *= .01f; //if within min range, massively negatively weight weapon - allows weapon to still be selected if all others lost/out of ammo } @@ -3865,7 +4505,7 @@ bool SmartPickWeapon_EngagementEnvelope(TargetInfo target) float candidateYTraverse = Rocket.yawRange; float candidatePTraverse = Rocket.maxPitch; float candidateMinrange = Rocket.engageRangeMin; - + float candidateMaxRange = Rocket.engageRangeMax; Transform fireTransform = Rocket.fireTransforms[0]; if (vessel.Splashed && (BDArmorySettings.BULLET_WATER_DRAG && FlightGlobals.getAltitudeAtPos(fireTransform.position) < 0)) continue; @@ -3887,7 +4527,7 @@ bool SmartPickWeapon_EngagementEnvelope(TargetInfo target) { candidateRPM *= 0.01f; //negatively weight against contact-fuze rockets } - if (candidateMinrange > distance) + if (candidateMinrange > distance || distance > candidateMaxRange) { candidateRPM *= .01f; //if within min range, massively negatively weight weapon - allows weapon to still be selected if all others lost/out of ammo } @@ -3907,7 +4547,7 @@ bool SmartPickWeapon_EngagementEnvelope(TargetInfo target) if (candidateClass == WeaponClasses.Missile) { - if (missilesAway >= maxMissilesOnTarget) continue;// Max missiles are fired, try another weapon + if (firedMissiles >= maxMissilesOnTarget) continue;// Max missiles are fired, try another weapon MissileLauncher mlauncher = item.Current as MissileLauncher; float candidateDetDist = 0; float candidateAccel = 0; //for anti-missile, prioritize proxidetonation and accel @@ -3916,9 +4556,12 @@ bool SmartPickWeapon_EngagementEnvelope(TargetInfo target) if (mlauncher != null) { + if (mlauncher.TargetingMode == MissileBase.TargetingModes.Radar && (!_radarsEnabled && !mlauncher.radarLOAL)) continue; //dont select RH missiles when no radar aboard + if (mlauncher.TargetingMode == MissileBase.TargetingModes.Laser && targetingPods.Count <= 0) continue; //don't select LH missiles when no FLIR aboard + if (mlauncher.reloadableRail != null && (mlauncher.reloadableRail.ammoCount < 1 && !BDArmorySettings.INFINITE_ORDINANCE)) continue; //don't select when out of ordinance candidateDetDist = mlauncher.DetonationDistance; candidateAccel = mlauncher.thrust / mlauncher.part.mass; //for anti-missile, prioritize proxidetonation and accel - bool EMP = mlauncher.EMP; + bool EMP = mlauncher.warheadType == MissileBase.WarheadTypes.EMP; candidatePriority = Mathf.RoundToInt(mlauncher.priority); if (EMP) continue; @@ -3961,7 +4604,7 @@ bool SmartPickWeapon_EngagementEnvelope(TargetInfo target) } //else if (!target.isLanded) - else if (target.isFlying) + else if (target.isFlying && !target.isMissile) { // iterate over weaponTypesAir and pick suitable one based on engagementRange (and dynamic launch zone for missiles) // Prioritize by: @@ -3969,6 +4612,7 @@ bool SmartPickWeapon_EngagementEnvelope(TargetInfo target) // 1. Lasers // 2. Guns // 3. rockets + // 4. unguided missiles using (List.Enumerator item = weaponTypesAir.GetEnumerator()) while (item.MoveNext()) { @@ -3978,8 +4622,70 @@ bool SmartPickWeapon_EngagementEnvelope(TargetInfo target) if (!CheckEngagementEnvelope(item.Current, distance)) continue; // weapon usable, if missile continue looking for lasers/guns, else take it WeaponClasses candidateClass = item.Current.GetWeaponClass(); - // any rocketpods work? + if (candidateClass == WeaponClasses.Bomb) //hardly ideal, but if it's the only thing you have, then just maybe... + { + if (!vessel.Splashed || (vessel.Splashed && vessel.altitude > currentTarget.Vessel.altitude)) + { + MissileLauncher Bomb = item.Current as MissileLauncher; + + if (Bomb.reloadableRail != null && (Bomb.reloadableRail.ammoCount < 1 && !BDArmorySettings.INFINITE_ORDINANCE)) continue; //don't select when out of ordinance + //if (firedMissiles >= maxMissilesOnTarget) continue;// Max missiles are fired, try another weapon + // only useful if we are flying + float candidateYield = Bomb.GetBlastRadius(); + int candidateCluster = Bomb.clusterbomb; + bool EMP = Bomb.warheadType == MissileBase.WarheadTypes.EMP; + int candidatePriority = Mathf.RoundToInt(Bomb.priority); + + if (EMP && target.isDebilitated) continue; + if (targetWeapon != null && targetWeaponPriority > candidatePriority) + continue; //keep higher priority weapon + if (distance < candidateYield) + continue;// don't drop bombs when within blast radius + bool candidateUnguided = false; + if (!vessel.LandedOrSplashed) + { + if (Bomb.GuidanceMode != MissileBase.GuidanceModes.AGMBallistic) //If you're targeting a massive flying sky cruiser or zeppelin, and you have *nothing else*... + { + candidateYield /= (candidateCluster * 2); //clusterbombs are altitude fuzed, not proximity + if (targetWeaponPriority < candidatePriority) //use priority bomb + { + targetWeapon = item.Current; + targetBombYield = candidateYield; + targetWeaponPriority = candidatePriority; + } + else //if equal priority, use standard weighting + { + if (targetBombYield < candidateYield)//prioritized by biggest boom + { + targetWeapon = item.Current; + targetBombYield = candidateYield; + targetWeaponPriority = candidatePriority; + } + } + candidateUnguided = true; + } + if (Bomb.GuidanceMode == MissileBase.GuidanceModes.AGMBallistic) //There is at least precedent for A2A JDAM kills, so thats something + { + if (targetWeaponPriority < candidatePriority) //use priority bomb + { + targetWeapon = item.Current; + targetBombYield = candidateYield; + targetWeaponPriority = candidatePriority; + } + else //if equal priority, use standard weighting + { + if ((candidateUnguided ? targetBombYield / 2 : targetBombYield) < candidateYield) //prioritize biggest Boom, but preference guided bombs + { + targetWeapon = item.Current; + targetBombYield = candidateYield; + targetWeaponPriority = candidatePriority; + } + } + } + } + } + } if (candidateClass == WeaponClasses.Rocket) { //for AA, favor higher accel and proxifuze @@ -3990,6 +4696,7 @@ bool SmartPickWeapon_EngagementEnvelope(TargetInfo target) int candidatePriority = Mathf.RoundToInt(Rocket.priority); float candidateYTraverse = Rocket.yawRange; float candidatePTraverse = Rocket.maxPitch; + float candidateMaxRange = Rocket.engageRangeMax; float candidateMinrange = Rocket.engageRangeMin; Transform fireTransform = Rocket.fireTransforms[0]; @@ -4031,10 +4738,11 @@ bool SmartPickWeapon_EngagementEnvelope(TargetInfo target) { candidateRPM *= .01f; //if outside firing angle, massively negatively weight weapon - allows weapon to still be selected if all others lost/out of ammo } - if (candidateMinrange > distance) + if (candidateMinrange > distance || distance > candidateMaxRange) { candidateRPM *= .01f; //if within min range massively negatively weight weapon - allows weapon to still be selected if all others lost/out of ammo } + if (Rocket.dualModeAPS) candidateRPM /= 4; //disincentivise selecting dual mode APS turrets if something else is available to maintain Point Defense umbrella candidateRPM /= 2; //halve rocket RPm to de-weight it against guns/lasers if (targetWeaponPriority < candidatePriority) //use priority gun @@ -4070,8 +4778,9 @@ bool SmartPickWeapon_EngagementEnvelope(TargetInfo target) bool candidateVTFuzed = Gun.eFuzeType == ModuleWeapon.FuzeTypes.Timed || Gun.eFuzeType == ModuleWeapon.FuzeTypes.Flak; float Cannistershot = Gun.ProjectileCount; float candidateMinrange = Gun.engageRangeMin; + float candidateMaxRange = Gun.engageRangeMax; int candidatePriority = Mathf.RoundToInt(Gun.priority); - float candidateRadius = currentTarget.Vessel.GetRadius(); + float candidateRadius = currentTarget.Vessel.GetRadius(Gun.fireTransforms[0].forward, target.bounds); float candidateCaliber = Gun.caliber; if (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 41) { @@ -4101,7 +4810,7 @@ bool SmartPickWeapon_EngagementEnvelope(TargetInfo target) { candidateCaliber *= .01f; //if outside firing angle, massively negatively weight weapon - allows weapon to still be selected if all others lost/out of ammo } - if (candidateMinrange > distance) + if (candidateMinrange > distance || distance > candidateMaxRange) { candidateCaliber *= .01f; //if within min range massively negatively weight weapon - allows weapon to still be selected if all others lost/out of ammo } @@ -4125,11 +4834,13 @@ bool SmartPickWeapon_EngagementEnvelope(TargetInfo target) { candidateRPM *= .01f; //if outside firing angle, massively negatively weight weapon - allows weapon to still be selected if all others lost/out of ammo } - if (candidateMinrange > distance) + if (candidateMinrange > distance || distance > candidateMaxRange) { candidateRPM *= .01f; //if within min range massively negatively weight weapon - allows weapon to still be selected if all others lost/out of ammo } } + if (Gun.dualModeAPS) candidateRPM /= 4; //disincentivise selecting dual mode APS turrets if something else is available to maintain Point Defense umbrella + if ((targetWeapon != null) && (targetWeapon.GetWeaponClass() == WeaponClasses.Missile) && (targetWeaponTDPS > 0)) continue; //dont replace missiles within their engage range @@ -4158,6 +4869,7 @@ bool SmartPickWeapon_EngagementEnvelope(TargetInfo target) bool candidateGimbal = Laser.turret; float candidateTraverse = Laser.yawRange; float candidateMinrange = Laser.engageRangeMin; + float candidateMaxRange = Laser.engageRangeMax; int candidatePriority = Mathf.RoundToInt(Laser.priority); bool electrolaser = Laser.electroLaser; bool pulseLaser = Laser.pulseLaser; @@ -4182,10 +4894,12 @@ bool SmartPickWeapon_EngagementEnvelope(TargetInfo target) { candidateRPM *= 1.5f; // weight selection towards turreted lasers } - if (candidateMinrange > distance) + if (candidateMinrange > distance || distance > candidateMaxRange) { candidateRPM *= .00001f; //if within min range massively negatively weight weapon - allows weapon to still be selected if all others lost/out of ammo } + if (Laser.dualModeAPS) candidateRPM /= 4; //disincentivise selecting dual mode APS turrets if something else is available to maintain Point Defense umbrella + if (targetWeaponPriority < candidatePriority) //use priority gun { targetWeapon = item.Current; @@ -4205,7 +4919,7 @@ bool SmartPickWeapon_EngagementEnvelope(TargetInfo target) //projectile weapon selected, any missiles that take precedence? if (candidateClass == WeaponClasses.Missile) { - if (missilesAway >= maxMissilesOnTarget) continue;// Max missiles are fired, try another weapon + //if (firedMissiles >= maxMissilesOnTarget) continue;// Max missiles are fired, try another weapon float candidateDetDist = 0; float candidateTurning = 0; int candidatePriority = 0; @@ -4214,12 +4928,15 @@ bool SmartPickWeapon_EngagementEnvelope(TargetInfo target) MissileLauncher mlauncher = item.Current as MissileLauncher; if (mlauncher != null) { + if (mlauncher.reloadableRail != null && (mlauncher.reloadableRail.ammoCount < 1 && !BDArmorySettings.INFINITE_ORDINANCE)) continue; //don't select when out of ordinance candidateDetDist = mlauncher.DetonationDistance; candidateTurning = mlauncher.maxTurnRateDPS; //for anti-aircraft, prioritize detonation dist and turn capability candidatePriority = Mathf.RoundToInt(mlauncher.priority); - bool EMP = mlauncher.EMP; - - if (EMP) continue; + bool EMP = mlauncher.warheadType == MissileBase.WarheadTypes.EMP; + bool heat = mlauncher.TargetingMode == MissileBase.TargetingModes.Heat; + bool radar = mlauncher.TargetingMode == MissileBase.TargetingModes.Radar; + float heatThresh = mlauncher.heatThreshold; + if (EMP && target.isDebilitated) continue; if (vessel.Splashed && (BDArmorySettings.BULLET_WATER_DRAG && FlightGlobals.getAltitudeAtPos(mlauncher.transform.position) < 0)) continue; if (targetWeapon != null && targetWeaponPriority > candidatePriority) continue; //keep higher priority weapon @@ -4232,6 +4949,21 @@ bool SmartPickWeapon_EngagementEnvelope(TargetInfo target) { candidateTDPS += candidateDetDist; // weight selection towards misiles with proximity warheads } + if (heat && heatTarget.exists && heatTarget.signalStrength < heatThresh) + { + candidateTDPS *= 0.001f; //Heatseeker, but IR sig is below missile threshold, skip to something else unless nutohine else available + } + if (radar) + { + if ((!_radarsEnabled || (vesselRadarData != null && !vesselRadarData.locked)) && !mlauncher.radarLOAL) + { + candidateTDPS *= 0.001f; //no radar lock, skip to something else unless nothing else available + } + } + if (mlauncher.TargetingMode == MissileBase.TargetingModes.Laser && targetingPods.Count <= 0) + { + candidateTDPS *= 0.001f; //no laserdot, skip to something else unless nothing else available + } } else { //is modular missile @@ -4254,11 +4986,12 @@ bool SmartPickWeapon_EngagementEnvelope(TargetInfo target) candidateTDPS += candidateDetDist; // weight selection towards misiles with proximity warheads } } - if (distance < ((EngageableWeapon)item.Current).engageRangeMin) + if (distance < ((EngageableWeapon)item.Current).engageRangeMin || firedMissiles >= maxMissilesOnTarget) candidateTDPS *= -1f; // if within min range, negatively weight weapon - allows weapon to still be selected if all others lost/out of ammo if ((!vessel.LandedOrSplashed) || ((distance > gunRange) && (vessel.LandedOrSplashed))) // If we're not airborne, we want to prioritize guns { + if (distance <= gunRange && candidateTDPS < 1 && targetWeapon != null) continue; //missiles are within min range/can't lock, don't replace existing gun if in gun range if (targetWeaponPriority < candidatePriority) //use priority gun { targetWeapon = item.Current; @@ -4304,6 +5037,7 @@ bool SmartPickWeapon_EngagementEnvelope(TargetInfo target) bool candidateGimbal = Laser.turret; float candidateTraverse = Laser.yawRange; float candidateMinrange = Laser.engageRangeMin; + float candidateMaxRange = Laser.engageRangeMax; int candidatePriority = Mathf.RoundToInt(Laser.priority); bool electrolaser = Laser.electroLaser; bool pulseLaser = Laser.pulseLaser; @@ -4333,10 +5067,12 @@ bool SmartPickWeapon_EngagementEnvelope(TargetInfo target) { candidateRPM *= 1.5f; // weight selection towards lasers that can do blast damage } - if (candidateMinrange > distance) + if (candidateMinrange > distance || distance > candidateMaxRange) { candidateRPM *= .00001f; //if within min range massively negatively weight weapon - allows weapon to still be selected if all others lost/out of ammo } + if (Laser.dualModeAPS) candidateRPM /= 4; //disincentivise selecting dual mode APS turrets if something else is available to maintain Point Defense umbrella + if (targetWeaponPriority < candidatePriority) //use priority gun { targetWeapon = item.Current; @@ -4366,8 +5102,9 @@ bool SmartPickWeapon_EngagementEnvelope(TargetInfo target) int candidatePriority = Mathf.RoundToInt(Gun.priority); bool candidateGimbal = Gun.turret; float candidateMinrange = Gun.engageRangeMin; + float candidateMaxRange = Gun.engageRangeMax; float candidateTraverse = Gun.yawRange * Gun.maxPitch; - float candidateRadius = currentTarget.Vessel.GetRadius(); + float candidateRadius = currentTarget.Vessel.GetRadius(Gun.fireTransforms[0].forward, target.bounds); float candidateCaliber = Gun.caliber; Transform fireTransform = Gun.fireTransforms[0]; @@ -4392,10 +5129,12 @@ bool SmartPickWeapon_EngagementEnvelope(TargetInfo target) { candidateRPM *= 1.5f; // weight selection towards turrets } - if (candidateMinrange > distance) + if (candidateMinrange > distance || distance > candidateMaxRange) { candidateRPM *= .01f; //if within min range massively negatively weight weapon - allows weapon to still be selected if all others lost/out of ammo } + if (Gun.dualModeAPS) candidateRPM /= 4; //disincentivise selecting dual mode APS turrets if something else is available to maintain Point Defense umbrella + if (targetWeaponPriority < candidatePriority) //use priority gun { targetWeapon = item.Current; @@ -4452,16 +5191,16 @@ bool SmartPickWeapon_EngagementEnvelope(TargetInfo target) if (candidateClass == WeaponClasses.Bomb && (!vessel.Splashed || (vessel.Splashed && vessel.altitude > currentTarget.Vessel.altitude))) //I guess depth charges would sorta apply here, but those are SLW instead { MissileLauncher Bomb = item.Current as MissileLauncher; - if (targetWeapon != null && targetWeapon.GetWeaponClass() == WeaponClasses.Missile) continue; - if (missilesAway >= maxMissilesOnTarget) continue;// Max missiles are fired, try another weapon + if (Bomb.reloadableRail != null && (Bomb.reloadableRail.ammoCount < 1 && !BDArmorySettings.INFINITE_ORDINANCE)) continue; //don't select when out of ordinance + //if (firedMissiles >= maxMissilesOnTarget) continue;// Max missiles are fired, try another weapon // only useful if we are flying float candidateYield = Bomb.GetBlastRadius(); int candidateCluster = Bomb.clusterbomb; - bool EMP = Bomb.EMP; + bool EMP = Bomb.warheadType == MissileBase.WarheadTypes.EMP; int candidatePriority = Mathf.RoundToInt(Bomb.priority); double srfSpeed = currentTarget.Vessel.horizontalSrfSpeed; - if (EMP) continue; + if (EMP && target.isDebilitated) continue; if (targetWeapon != null && targetWeaponPriority > candidatePriority) continue; //keep higher priority weapon if (distance < candidateYield) @@ -4474,7 +5213,7 @@ bool SmartPickWeapon_EngagementEnvelope(TargetInfo target) // - by blast strength // - find way to implement cluster bomb selection priority? - if (Bomb.GuidanceMode == MissileBase.GuidanceModes.None) + if (Bomb.GuidanceMode != MissileBase.GuidanceModes.AGMBallistic) { if (targetWeaponPriority < candidatePriority) //use priority bomb { @@ -4540,71 +5279,130 @@ bool SmartPickWeapon_EngagementEnvelope(TargetInfo target) // - guided missiles // - by blast strength // - add code to choose optimal missile based on target profile - i.e. use bigger bombs on large landcruisers, smaller bombs on small Vees that don't warrant that sort of overkill? - MissileLauncher Missile = item.Current as MissileLauncher; - if (vessel.Splashed && FlightGlobals.getAltitudeAtPos(item.Current.GetPart().transform.position) < -2) continue; - if (missilesAway >= maxMissilesOnTarget) continue;// Max missiles are fired, try another weapon - float candidateYield = Missile.GetBlastRadius(); + int candidatePriority; + float candidateYield; double srfSpeed = currentTarget.Vessel.horizontalSrfSpeed; - bool EMP = Missile.EMP; - int candidatePriority = Mathf.RoundToInt(Missile.priority); - - if (EMP && target.isDebilitated) continue; - //if (targetWeapon != null && targetWeapon.GetWeaponClass() == WeaponClasses.Bomb) targetYield = -1; //reset targetyield so larger bomb yields don't supercede missiles - if (targetWeapon != null && targetWeaponPriority > candidatePriority) - continue; //keep higher priority weapon - if (srfSpeed < 1) // set higher than 0 in case of physics jitteriness + MissileLauncher Missile = item.Current as MissileLauncher; + if (Missile != null) { - if (Missile.TargetingMode == MissileBase.TargetingModes.Gps || - (Missile.GuidanceMode == MissileBase.GuidanceModes.Cruise || - Missile.GuidanceMode == MissileBase.GuidanceModes.AGMBallistic || - Missile.GuidanceMode == MissileBase.GuidanceModes.None)) + //if (Missile.TargetingMode == MissileBase.TargetingModes.Radar && radars.Count <= 0) continue; //dont select RH missiles when no radar aboard + //if (Missile.TargetingMode == MissileBase.TargetingModes.Laser && targetingPods.Count <= 0) continue; //don't select LH missiles when no FLIR aboard + if (Missile.reloadableRail != null && (Missile.reloadableRail.ammoCount < 1 && !BDArmorySettings.INFINITE_ORDINANCE)) continue; //don't select when out of ordinance + if (vessel.Splashed && FlightGlobals.getAltitudeAtPos(item.Current.GetPart().transform.position) < -2) continue; + //if (firedMissiles >= maxMissilesOnTarget) continue;// Max missiles are fired, try another weapon + candidateYield = Missile.GetBlastRadius(); + bool EMP = Missile.warheadType == MissileBase.WarheadTypes.EMP; + candidatePriority = Mathf.RoundToInt(Missile.priority); + + if (EMP && target.isDebilitated) continue; + //if (targetWeapon != null && targetWeapon.GetWeaponClass() == WeaponClasses.Bomb) targetYield = -1; //reset targetyield so larger bomb yields don't supercede missiles + if (targetWeapon != null && targetWeaponPriority > candidatePriority) + continue; //keep higher priority weapon + if (srfSpeed < 1) // set higher than 0 in case of physics jitteriness { - if (targetWeapon != null && targetYield > candidateYield) continue; //prioritize biggest Boom - targetYield = candidateYield; - candidateAGM = true; - targetWeapon = item.Current; - if (distance > Missile.engageRangeMin) - break; + if (Missile.TargetingMode == MissileBase.TargetingModes.Gps || + Missile.GuidanceMode == MissileBase.GuidanceModes.Cruise || + Missile.GuidanceMode == MissileBase.GuidanceModes.AGMBallistic || + Missile.GuidanceMode == MissileBase.GuidanceModes.None) + { + if (targetWeapon != null && targetYield > candidateYield) continue; //prioritize biggest Boom + if (distance < Missile.engageRangeMin) continue; //select missiles we can use now + targetYield = candidateYield; + candidateAGM = true; + targetWeapon = item.Current; + } } - } - if (Missile.TargetingMode == MissileBase.TargetingModes.AntiRad && (rwr && rwr.rwrEnabled)) - {// make it so this only selects antirad when hostile radar - for (int i = 0; i < rwr.pingsData.Length; i++) - { - if (Missile.antiradTargets.Contains(rwr.pingsData[i].signalStrength)) + if (Missile.TargetingMode == MissileBase.TargetingModes.AntiRad && (rwr && rwr.rwrEnabled)) + {// make it so this only selects antirad when hostile radar + for (int i = 0; i < rwr.pingsData.Length; i++) { - if ((rwr.pingWorldPositions[i] - guardTarget.CoM).sqrMagnitude < 20 * 20) //is current target a hostile radar source? + if (Missile.antiradTargets.Contains(rwr.pingsData[i].signalStrength)) { - candidateAntiRad = true; + if ((rwr.pingWorldPositions[i] - guardTarget.CoM).sqrMagnitude < 20 * 20) //is current target a hostile radar source? + { + candidateAntiRad = true; + candidateYield *= 2; // Prioritize anti-rad missiles for hostile radar sources + } } } + if (candidateAntiRad) + { + if (targetWeapon != null && targetYield > candidateYield) continue; //prioritize biggest Boom + targetYield = candidateYield; + targetWeapon = item.Current; + targetWeaponPriority = candidatePriority; + candidateAGM = true; + } } - if (candidateAntiRad) + else if (Missile.TargetingMode == MissileBase.TargetingModes.Laser) { + if (candidateAntiRad) continue; //keep antirad missile; + if (Missile.TargetingMode == MissileBase.TargetingModes.Laser && targetingPods.Count <= 0) candidateYield *= 0.1f; + if (targetWeapon != null && targetYield > candidateYield) continue; //prioritize biggest Boom + candidateAGM = true; targetYield = candidateYield; targetWeapon = item.Current; targetWeaponPriority = candidatePriority; - candidateAGM = true; + } + else + { + if (!candidateAGM) + { + if (Missile.TargetingMode == MissileBase.TargetingModes.Radar && (!_radarsEnabled || (vesselRadarData != null && !vesselRadarData.locked)) && !Missile.radarLOAL) candidateYield *= 0.1f; + if (targetWeapon != null && targetYield > candidateYield) continue; + targetYield = candidateYield; + targetWeapon = item.Current; + targetWeaponPriority = candidatePriority; + } } } - else if (Missile.TargetingMode == MissileBase.TargetingModes.Laser) - { - if ((targetWeapon != null && targetYield > candidateYield) && !candidateAntiRad) continue; - candidateAGM = true; - targetYield = candidateYield; - targetWeapon = item.Current; - targetWeaponPriority = candidatePriority; - } - else + else //modular missile { - if (!candidateAGM) + BDModularGuidance mm = item.Current as BDModularGuidance; //need to work out priority stuff for MMGs + if (mm.GuidanceMode == MissileBase.GuidanceModes.SLW) continue; + candidateYield = mm.warheadYield; + //candidateTurning = ((MissileLauncher)item.Current).maxTurnRateDPS; //for anti-aircraft, prioritize detonation dist and turn capability + candidatePriority = Mathf.RoundToInt(mm.priority); + + if (vessel.Splashed && (BDArmorySettings.BULLET_WATER_DRAG && FlightGlobals.getAltitudeAtPos(mm.transform.position) < 0)) continue; + if (targetWeapon != null && targetWeaponPriority > candidatePriority) continue; //keep higher priority weapon + if (srfSpeed < 1) // set higher than 0 in case of physics jitteriness + { + if (mm.TargetingMode == MissileBase.TargetingModes.Gps || + mm.GuidanceMode == MissileBase.GuidanceModes.Cruise || + mm.GuidanceMode == MissileBase.GuidanceModes.AGMBallistic || + mm.GuidanceMode == MissileBase.GuidanceModes.None) + { + if (targetWeapon != null && targetYield > candidateYield) continue; //prioritize biggest Boom + if (distance < mm.engageRangeMin) continue; //select missiles we can use now + targetYield = candidateYield; + candidateAGM = true; + targetWeapon = item.Current; + } + } + if (mm.TargetingMode == MissileBase.TargetingModes.Laser) { - if (targetWeapon != null && targetYield > candidateYield) continue; + if (candidateAntiRad) continue; //keep antirad missile; + if (mm.TargetingMode == MissileBase.TargetingModes.Laser && targetingPods.Count <= 0) candidateYield *= 0.1f; + + if (targetWeapon != null && targetYield > candidateYield) continue; //prioritize biggest Boom + candidateAGM = true; targetYield = candidateYield; targetWeapon = item.Current; targetWeaponPriority = candidatePriority; } + else + { + if (!candidateAGM) + { + if (Missile.TargetingMode == MissileBase.TargetingModes.Radar && (!_radarsEnabled || (vesselRadarData != null && !vesselRadarData.locked)) && !Missile.radarLOAL) candidateYield *= 0.1f; + if (targetWeapon != null && targetYield > candidateYield) continue; + targetYield = candidateYield; + targetWeapon = item.Current; + targetWeaponPriority = candidatePriority; + } + } } } @@ -4614,10 +5412,11 @@ bool SmartPickWeapon_EngagementEnvelope(TargetInfo target) // almost as good as STS missiles, which we don't have. if (candidateClass == WeaponClasses.SLW && target.isSplashed) { - if (missilesAway >= maxMissilesOnTarget) continue;// Max missiles are fired, try another weapon + //if (firedMissiles >= maxMissilesOnTarget) continue;// Max missiles are fired, try another weapon MissileLauncher SLW = item.Current as MissileLauncher; + if (SLW.reloadableRail != null && (SLW.reloadableRail.ammoCount < 1 && !BDArmorySettings.INFINITE_ORDINANCE)) continue; //don't select when out of ordinance float candidateYield = SLW.GetBlastRadius(); - bool EMP = SLW.EMP; + bool EMP = SLW.warheadType == MissileBase.WarheadTypes.EMP; int candidatePriority = Mathf.RoundToInt(SLW.priority); if (EMP && target.isDebilitated) continue; @@ -4654,8 +5453,11 @@ bool SmartPickWeapon_EngagementEnvelope(TargetInfo target) if (candidateClass == WeaponClasses.SLW) { MissileLauncher SLW = item.Current as MissileLauncher; + if (SLW.TargetingMode == MissileBase.TargetingModes.Radar && (!_radarsEnabled && !SLW.radarLOAL)) continue; //dont select RH missiles when no radar aboard + if (SLW.TargetingMode == MissileBase.TargetingModes.Laser && targetingPods.Count <= 0) continue; //don't select LH missiles when no FLIR aboard + if (SLW.reloadableRail != null && (SLW.reloadableRail.ammoCount < 1 && !BDArmorySettings.INFINITE_ORDINANCE)) continue; //don't select when out of ordinance float candidateYield = SLW.GetBlastRadius(); - bool EMP = SLW.EMP; + bool EMP = SLW.warheadType == MissileBase.WarheadTypes.EMP; int candidatePriority = Mathf.RoundToInt(SLW.priority); if (targetWeapon != null && targetWeaponPriority > candidatePriority) @@ -4677,7 +5479,7 @@ bool SmartPickWeapon_EngagementEnvelope(TargetInfo target) { if (item.Current.GetMissileType().ToLower() != "torpedo") continue; if (distance < candidateYield) continue; //don't use explosives within their blast radius - if (missilesAway >= maxMissilesOnTarget) continue;// Max missiles are fired, try another weapon + //if(firedMissiles >= maxMissilesOnTarget) continue;// Max missiles are fired, try another weapon targetWeapon = item.Current; targetWeaponPriority = candidatePriority; break; @@ -4758,10 +5560,12 @@ bool SmartPickWeapon_EngagementEnvelope(TargetInfo target) { candidateRPM *= 1.5f; // weight selection towards turreted lasers } - if (candidateMinrange > distance) + if (candidateMinrange > distance || distance > candidateMaxrange / 10) { candidateRPM *= .00001f; //if within min range massively negatively weight weapon - allows weapon to still be selected if all others lost/out of ammo } + if (Laser.dualModeAPS) candidateRPM /= 4; //disincentivise selecting dual mode APS turrets if something else is available to maintain Point Defense umbrella + if (targetWeaponPriority < candidatePriority) //use priority gun { targetWeapon = item.Current; @@ -4791,15 +5595,15 @@ bool SmartPickWeapon_EngagementEnvelope(TargetInfo target) for (int i = 1; i < weaponArray.Length; i++) { weaponIndex = i; - if (selectedWeapon.GetShortName() == weaponArray[weaponIndex].GetShortName()) + if (selectedWeapon.GetShortName() == weaponArray[weaponIndex].GetShortName() && targetWeapon.GetEngageRange() == weaponArray[weaponIndex].GetEngageRange()) { break; } } - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_AI) { - Debug.Log("[BDArmory.MissileFire]: " + vessel.vesselName + " - Selected weapon " + selectedWeapon.GetShortName()); + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} - Selected weapon {selectedWeapon.GetShortName()}"); } PrepareWeapons(); @@ -4809,9 +5613,9 @@ bool SmartPickWeapon_EngagementEnvelope(TargetInfo target) } else { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_AI) { - Debug.Log("[BDArmory.MissileFire]: " + vessel.vesselName + " - No weapon selected for target " + target.Vessel.vesselName); + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} - No weapon selected for target {target.Vessel.vesselName}"); // Debug.Log("DEBUG target isflying:" + target.isFlying + ", isLorS:" + target.isLandedOrSurfaceSplashed + ", isUW:" + target.isUnderwater); // if (target.isFlying) // foreach (var weapon in weaponTypesAir) @@ -4826,7 +5630,6 @@ bool SmartPickWeapon_EngagementEnvelope(TargetInfo target) // Debug.Log("DEBUG landed target:" + target.Vessel + ", weapon:" + weapon + " can engage:" + CheckEngagementEnvelope(weapon, distance) + ", engageEnabled:" + engageableWeapon.engageEnabled + ", min/max:" + engageableWeapon.GetEngagementRangeMin() + "/" + engageableWeapon.GetEngagementRangeMax()); // } } - selectedWeapon = null; weaponIndex = 0; return false; @@ -4842,7 +5645,8 @@ bool CheckEngagementEnvelope(IBDWeapon weaponCandidate, float distanceToTarget) if (!engageableWeapon.engageEnabled) return true; //if (distanceToTarget < engageableWeapon.GetEngagementRangeMin()) return false; //covered in weapon select logic //if (distanceToTarget > engageableWeapon.GetEngagementRangeMax()) return false; - if (distanceToTarget > (engageableWeapon.GetEngagementRangeMax() * 1.2f)) return false; //have Ai begin to preemptively lead target, instead of frantically doing so after weapon in range + //if (distanceToTarget > (engageableWeapon.GetEngagementRangeMax() * 1.2f)) return false; //have Ai begin to preemptively lead target, instead of frantically doing so after weapon in range + if (distanceToTarget > (engageableWeapon.GetEngagementRangeMax() + (float)vessel.speed * 2)) return false; //have AI preemptively begin to lead 2s out from max weapon range switch (weaponCandidate.GetWeaponClass()) { @@ -4867,9 +5671,9 @@ bool CheckEngagementEnvelope(IBDWeapon weaponCandidate, float distanceToTarget) // check ammo if (CheckAmmo(laser)) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_WEAPONS) { - Debug.Log("[BDArmory.MissileFire]: " + vessel.vesselName + " - Firing possible with " + weaponCandidate.GetShortName()); + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} - Firing possible with {weaponCandidate.GetShortName()}"); } return true; } @@ -4888,18 +5692,18 @@ bool CheckEngagementEnvelope(IBDWeapon weaponCandidate, float distanceToTarget) return false; // check overheat, reloading, ability to fire soon - if (gun.isOverheated) + if (!gun.hasGunner) return false; - if (gun.isReloading || !gun.hasGunner) + if (gun.isReloading || gun.isOverheated) return false; if (!gun.CanFireSoon()) return false; // check ammo if (CheckAmmo(gun)) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_WEAPONS) { - Debug.Log("[BDArmory.MissileFire]: " + vessel.vesselName + " - Firing possible with " + weaponCandidate.GetShortName()); + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} - Firing possible with {weaponCandidate.GetShortName()}"); } return true; } @@ -4910,6 +5714,16 @@ bool CheckEngagementEnvelope(IBDWeapon weaponCandidate, float distanceToTarget) { MissileBase ml = (MissileBase)weaponCandidate; if (distanceToTarget < engageableWeapon.GetEngagementRangeMin()) return false; + bool readyMissiles = false; + using (var msl = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) + while (msl.MoveNext()) + { + if (msl.Current == null) continue; + if (msl.Current.launched) continue; + readyMissiles = true; + break; + } + if (!readyMissiles) return false; // lock radar if needed if (ml.TargetingMode == MissileBase.TargetingModes.Radar) { @@ -4918,26 +5732,48 @@ bool CheckEngagementEnvelope(IBDWeapon weaponCandidate, float distanceToTarget) while (rd.MoveNext()) { if (rd.Current != null || rd.Current.canLock) + { rd.Current.EnableRadar(); + } } } - // check DLZ - MissileLaunchParams dlz = MissileLaunchParams.GetDynamicLaunchParams(ml, guardTarget.Velocity(), guardTarget.transform.position); + if (ml.TargetingMode == MissileBase.TargetingModes.Laser) + { + if (targetingPods.Count > 0) //if targeting pods are available, slew them onto target and lock. + { + using (List.Enumerator tgp = targetingPods.GetEnumerator()) + while (tgp.MoveNext()) + { + if (tgp.Current == null) continue; + tgp.Current.EnableCamera(); + } + } + } + // check DLZ + + MissileLaunchParams dlz = MissileLaunchParams.GetDynamicLaunchParams(ml, guardTarget.Velocity(), guardTarget.transform.position, -1, + (ml.TargetingMode == MissileBase.TargetingModes.Laser && BDATargetManager.ActiveLasers.Count <= 0 || ml.TargetingMode == MissileBase.TargetingModes.Radar && !_radarsEnabled && !ml.radarLOAL)); if (vessel.srfSpeed > ml.minLaunchSpeed && distanceToTarget < dlz.maxLaunchRange && distanceToTarget > dlz.minLaunchRange) { return true; } - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_MISSILES) { - Debug.Log("[BDArmory.MissileFire]: " + vessel.vesselName + " - Failed DLZ test: " + weaponCandidate.GetShortName() + ", distance: " + distanceToTarget + ", DLZ min/max: " + dlz.minLaunchRange + "/" + dlz.maxLaunchRange); + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} - Failed DLZ test: {weaponCandidate.GetShortName()}, distance: {distanceToTarget}, DLZ min/max: {dlz.minLaunchRange}/{dlz.maxLaunchRange}"); } break; } case WeaponClasses.Bomb: if (distanceToTarget < engageableWeapon.GetEngagementRangeMin()) return false; - if (!vessel.LandedOrSplashed) - return true; // TODO: bomb always allowed? + if (!vessel.LandedOrSplashed) // TODO: bomb always allowed? + using (var bomb = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) + while (bomb.MoveNext()) + { + if (bomb.Current == null) continue; + if (bomb.Current.launched) continue; + return true; + } break; case WeaponClasses.Rocket: @@ -4959,9 +5795,9 @@ bool CheckEngagementEnvelope(IBDWeapon weaponCandidate, float distanceToTarget) // check ammo if (CheckAmmo(rocket)) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_WEAPONS) { - Debug.Log("[BDArmory.MissileFire]: " + vessel.vesselName + " - Firing possible with " + weaponCandidate.GetShortName()); + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} - Firing possible with {weaponCandidate.GetShortName()}"); } return true; } @@ -5004,10 +5840,41 @@ public void SetTarget(TargetInfo target) } currentTarget = target; guardTarget = target.Vessel; - if (multiTargetNum > 1) + if (multiTargetNum > 1 || multiMissileTgtNum > 1) { SmartFindSecondaryTargets(); } + MissileBase ml = CurrentMissile; + MissileBase pMl = PreviousMissile; + if (!ml && pMl) ml = PreviousMissile; //if fired missile, then switched to guns or something + if (ml && ml.TargetingMode == MissileBase.TargetingModes.Radar && vesselRadarData != null && (!vesselRadarData.locked || vesselRadarData.lockedTargetData.vessel != guardTarget)) + { + if (!vesselRadarData.locked) + { + vesselRadarData.TryLockTarget(guardTarget); + } + else + { + if (firedMissiles >= maxMissilesOnTarget && (multiMissileTgtNum > 1 && BDATargetManager.TargetList(Team).Count > 1)) //if there are multiple potential targets, see how many can be fired at with missiles + { + if (!ml.radarLOAL) //switch active lock instead of clearing locks for SARH missiles + { + //vesselRadarData.UnlockCurrentTarget(); + vesselRadarData.TryLockTarget(guardTarget); + } + else + vesselRadarData.SwitchActiveLockedTarget(guardTarget); + } + else + { + if (PreviousMissile.ActiveRadar) //previous missile has gone active, don't need that lock anymore + { + vesselRadarData.UnlockSelectedTarget(PreviousMissile.targetVessel.Vessel); + } + vesselRadarData.TryLockTarget(guardTarget); + } + } + } } else // No target, disengage { @@ -5021,27 +5888,78 @@ public void SetTarget(TargetInfo target) } #endregion Smart Targeting - - public bool CanSeeTarget(TargetInfo target) + public float detectedTargetTimeout = 0; + public bool staleTarget = false; + public bool CanSeeTarget(TargetInfo target, bool checkForstaleTarget = true) { // fix cheating: we can see a target IF we either have a visual on it, OR it has been detected on radar/sonar // but to prevent AI from stopping an engagement just because a target dropped behind a small hill 5 seconds ago, clamp the timeout to 30 seconds // i.e. let's have at least some object permanence :) - // (Ideally, I'd love to have "stale targets", where AI would attack the last known position, but that's a feature for the future) - if (target.detectedTime.TryGetValue(Team, out float detectedTime) && Time.time - detectedTime < Mathf.Max(targetScanInterval, 30)) - return true; + // If we can't directly see the target via sight or radar, AI will head to last known position of target, based on target's vector at time contact was lost, + // with precision of estimated position degrading over time. + + //extend to allow teamamtes provide vision? Could count scouted tarets as stale to prevent precise targeting, but at least let AI know something is out there // can we get a visual sight of the target? - if ((target.Vessel.transform.position - transform.position).sqrMagnitude < guardRange * guardRange) + VesselCloakInfo vesselcamo = target.Vessel.gameObject.GetComponent(); + float viewModifier = 1; + if (vesselcamo && vesselcamo.cloakEnabled) + { + viewModifier = vesselcamo.opticalReductionFactor; + } + //Can the target be seen? + if ((target.Vessel.transform.position - transform.position).sqrMagnitude < (guardRange * viewModifier) * (guardRange * viewModifier) && + Vector3.Angle(-vessel.ReferenceTransform.forward, target.Vessel.transform.position - vessel.CoM) < guardAngle / 2) { - if (RadarUtils.TerrainCheck(target.Vessel.transform.position, transform.position)) + if (RadarUtils.TerrainCheck(target.Vessel.transform.position, transform.position)) //vessel behind terrain { + if (target.detectedTime.TryGetValue(Team, out float detectedTime) && Time.time - detectedTime < Mathf.Max(30, targetScanInterval)) + { + //Debug.Log($"[BDArmory.MissileFire]: {target.name} last seen {Time.time - detectedTime} seconds ago. Recalling last known position"); + detectedTargetTimeout = Time.time - detectedTime; + staleTarget = true; + return true; + } + staleTarget = true; return false; } - + detectedTargetTimeout = 0; + staleTarget = false; return true; } - + //target beyond visual range. Detected by radar/IRST? + target.detected.TryGetValue(Team, out bool detected);//see if the target is actually within radar sight right now + if (detected) + { + detectedTargetTimeout = 0; + staleTarget = false; + return true; + } + //carrying antirads and picking up RWR pings? + if (rwr && rwr.rwrEnabled && rwr.displayRWR && hasAntiRadiationOrdinance)//see if RWR is picking up a ping from unseen radar source and craft has HARMs + { + for (int i = 0; i < rwr.pingsData.Length; i++) //using copy of antirad targets due to CanSee running before weapon selection + { + if (rwr.pingsData[i].exists && antiradTargets.Contains(rwr.pingsData[i].signalStrength) && (rwr.pingWorldPositions[i] - target.position).sqrMagnitude < 20 * 20) + { + detectedTargetTimeout = 0; + staleTarget = false; + return true; + } + } + } + //can't see target, but did we see it recently? + if (checkForstaleTarget) //merely look to see if a target was last detected within 30s + { + if (target.detectedTime.TryGetValue(Team, out float detectedTime) && Time.time - detectedTime < Mathf.Max(30, targetScanInterval)) + { + //Debug.Log($"[BDArmory.MissileFire]: {target.name} last seen {Time.time - detectedTime} seconds ago. Recalling last known position"); + detectedTargetTimeout = Time.time - detectedTime; + staleTarget = true; + return true; + } + return false; //target long gone + } return false; } @@ -5050,10 +5968,15 @@ public bool CanSeeTarget(TargetInfo target) /// /// /// - public bool CanSeeTarget(Vessel target) + public bool CanSeeTarget(MissileBase target) { // can we get a visual sight of the target? - if ((target.transform.position - transform.position).sqrMagnitude < guardRange * guardRange) + float visrange = guardRange; + if (BDArmorySettings.VARIABLE_MISSILE_VISIBILITY) + { + visrange *= target.MissileState == MissileBase.MissileStates.Boost ? 1 : (target.MissileState == MissileBase.MissileStates.Cruise ? 0.75f : 0.33f); + } + if ((target.transform.position - transform.position).sqrMagnitude < visrange * visrange) { if (RadarUtils.TerrainCheck(target.transform.position, transform.position)) { @@ -5069,7 +5992,7 @@ public bool CanSeeTarget(Vessel target) void SearchForRadarSource() { antiRadTargetAcquired = false; - + antiRadiationTarget = Vector3.zero; if (rwr && rwr.rwrEnabled) { float closestAngle = 360; @@ -5141,17 +6064,24 @@ void SearchForHeatTarget() float scanRadius = CurrentMissile.lockedSensorFOV * 0.5f; float maxOffBoresight = CurrentMissile.maxOffBoresight * 0.85f; - if (vesselRadarData && vesselRadarData.locked) + if (vesselRadarData) // && !CurrentMissile.IndependantSeeker) //missile with independantSeeker can't get targetdata from radar/IRST { - heatTarget = vesselRadarData.lockedTargetData.targetData; + if (vesselRadarData.irstCount > 0) + { + heatTarget = vesselRadarData.activeIRTarget(); //point seeker at active target's IR return + } + else + { + if (vesselRadarData.locked) //uncaged radar lock + heatTarget = vesselRadarData.lockedTargetData.targetData; + } } - Vector3 direction = heatTarget.exists && Vector3.Angle(heatTarget.position - CurrentMissile.MissileReferenceTransform.position, CurrentMissile.GetForwardTransform()) < maxOffBoresight ? heatTarget.predictedPosition - CurrentMissile.MissileReferenceTransform.position : CurrentMissile.GetForwardTransform(); - - heatTarget = BDATargetManager.GetHeatTarget(vessel, vessel, new Ray(CurrentMissile.MissileReferenceTransform.position + (50 * CurrentMissile.GetForwardTransform()), direction), TargetSignatureData.noTarget, scanRadius, CurrentMissile.heatThreshold, CurrentMissile.allAspect, CurrentMissile.lockedSensorFOVBias, CurrentMissile.lockedSensorVelocityBias, this); + // remove AI target check/move to a missile .cfg option to allow older gen heaters? + heatTarget = BDATargetManager.GetHeatTarget(vessel, vessel, new Ray(CurrentMissile.MissileReferenceTransform.position + (50 * CurrentMissile.GetForwardTransform()), direction), TargetSignatureData.noTarget, scanRadius, CurrentMissile.heatThreshold, CurrentMissile.frontAspectHeatModifier, CurrentMissile.uncagedLock, CurrentMissile.lockedSensorFOVBias, CurrentMissile.lockedSensorVelocityBias, this, guardMode ? currentTarget : null); } } @@ -5173,11 +6103,33 @@ bool CrossCheckWithRWR(TargetInfo v) return matchFound; } - public void SendTargetDataToMissile(MissileBase ml) + public void SendTargetDataToMissile(MissileBase ml, bool clearHeat = true) { //TODO BDModularGuidance: implement all targetings on base - if (ml.TargetingMode == MissileBase.TargetingModes.Laser && laserPointDetected) + if (ml.TargetingMode == MissileBase.TargetingModes.Laser) { - ml.lockedCamera = foundCam; + if (laserPointDetected) + { + ml.lockedCamera = foundCam; + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log("[BDArmory.MissileData]: Sending targetInfo to laser Missile..."); + if (guardMode && ((foundCam.groundTargetPosition - guardTarget.CoM).sqrMagnitude < 10 * 10)) + { + ml.targetVessel = guardTarget.gameObject.GetComponent(); + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log($"[BDArmory.MissileData]: targetInfo sent for {ml.targetVessel.Vessel.GetName()}"); + } + } + else + { + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log("[BDArmory.MissileData]: Sending targetInfo to dumbfire laser Missile..."); + if (guardMode && guardTarget != null) + { + ml.targetVessel = guardTarget.gameObject.GetComponent(); + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log($"[BDArmory.MissileData]: targetInfo sent for {ml.targetVessel.Vessel.GetName()}"); + } + } } else if (ml.TargetingMode == MissileBase.TargetingModes.Gps) { @@ -5185,24 +6137,101 @@ public void SendTargetDataToMissile(MissileBase ml) { ml.targetGPSCoords = designatedGPSCoords; ml.TargetAcquired = true; + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log("[BDArmory.MissileData]: Sending targetInfo to GPS Missile..."); + if (guardMode && GPSDistanceCheck()) + { + ml.targetVessel = guardTarget.gameObject.GetComponent(); + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log($"[BDArmory.MissileData]: targetInfo sent for {ml.targetVessel.Vessel.GetName()}"); + } } } else if (ml.TargetingMode == MissileBase.TargetingModes.Heat && heatTarget.exists) { ml.heatTarget = heatTarget; - heatTarget = TargetSignatureData.noTarget; + MissileLauncher mml = ml as MissileLauncher; + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log("[BDArmory.MissileData]: Missile multi-launcher or reloadable rail found..."); + if (clearHeat) heatTarget = TargetSignatureData.noTarget; + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log("[BDArmory.MissileData]: Sending targetInfo to heat Missile..."); + ml.targetVessel = ml.heatTarget.vessel.gameObject.GetComponent(); + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log($"[BDArmory.MissileData]: targetInfo sent for {ml.targetVessel.Vessel.GetName()}"); } - else if (ml.TargetingMode == MissileBase.TargetingModes.Radar && vesselRadarData && vesselRadarData.locked)//&& radar && radar.lockedTarget.exists) + else if (ml.TargetingMode == MissileBase.TargetingModes.Radar) { - ml.radarTarget = vesselRadarData.lockedTargetData.targetData; - ml.vrd = vesselRadarData; - vesselRadarData.LastMissile = ml; + if (vesselRadarData && vesselRadarData.locked)//&& radar && radar.lockedTarget.exists) + { + if (guardTarget != null) + { + List possibleTargets = vesselRadarData.GetLockedTargets(); + for (int i = 0; i < possibleTargets.Count; i++) + { + if (possibleTargets[i].vessel == guardTarget) + { + ml.radarTarget = possibleTargets[i]; //send correct targetlock if firing multiple SARH missiles + } + } + } + else ml.radarTarget = vesselRadarData.lockedTargetData.targetData; + ml.vrd = vesselRadarData; + vesselRadarData.LastMissile = ml; + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log("[BDArmory.MissileData]: Sending targetInfo to radar Missile..."); + ml.targetVessel = vesselRadarData.lockedTargetData.targetData.vessel.gameObject.GetComponent(); + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log($"[BDArmory.MissileData]: targetInfo sent for {ml.targetVessel.Vessel.GetName()}"); + } + else + { + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log("[BDArmory.MissileData]: Sending targetInfo to dumbfire radar Missile..."); + if (guardMode && guardTarget != null) + { + ml.targetVessel = guardTarget.gameObject.GetComponent(); + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log($"[BDArmory.MissileData]: targetInfo sent for {ml.targetVessel.Vessel.GetName()}"); + } + } } - else if (ml.TargetingMode == MissileBase.TargetingModes.AntiRad && antiRadTargetAcquired) + else if (ml.TargetingMode == MissileBase.TargetingModes.AntiRad && antiRadTargetAcquired && antiRadiationTarget != Vector3.zero) { ml.TargetAcquired = true; ml.targetGPSCoords = VectorUtils.WorldPositionToGeoCoords(antiRadiationTarget, vessel.mainBody); + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log("[BDArmory.MissileData]: Sending targetInfo to Antirad Missile..."); + if (guardMode && AntiRadDistanceCheck()) + { + ml.targetVessel = guardTarget.gameObject.GetComponent(); + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log($"[BDArmory.MissileData]: targetInfo sent for {ml.targetVessel.Vessel.GetName()}"); + } + } + else if (ml.GetWeaponClass() == WeaponClasses.Bomb) + { + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log("[BDArmory.MissileData]: Sending targetInfo to bomb..."); + //if (guardMode && ((bombAimerPosition - guardTarget.CoM).sqrMagnitude < ml.GetBlastRadius())) + if (guardMode && guardTarget != null) + { + ml.targetVessel = guardTarget.gameObject.GetComponent(); + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log($"[BDArmory.MissileData]: targetInfo sent for {ml.targetVessel.Vessel.GetName()}"); + } + } + //ml.targetVessel = currentTarget; + if (currentTarget != null) + { + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log($"[BDArmory.MissileData]: firing missile at {currentTarget.Vessel.GetName()}"); + } + else + { + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log("[BDArmory.MissileData]: firing missile null target"); } } @@ -5234,15 +6263,13 @@ void GuardMode() { if (weapon.Current.turret != null && (weapon.Current.ammoCount > 0 || BDArmorySettings.INFINITE_AMMO)) // Put other turrets into standby instead of disabling them if they have ammo. { - if (!weapon.Current.isAPS) - { - weapon.Current.StandbyWeapon(); - weapon.Current.aiControlled = true; - } + weapon.Current.StandbyWeapon(); + weapon.Current.aiControlled = true; } continue; } weapon.Current.EnableWeapon(); + if (weapon.Current.dualModeAPS) weapon.Current.isAPS = false; weapon.Current.aiControlled = true; if (weapon.Current.FireAngleOverride) continue; // if a weapon-specific accuracy override is present weapon.Current.maxAutoFireCosAngle = adjustedAutoFireCosAngle; //user-adjustable from 0-2deg @@ -5256,6 +6283,7 @@ void GuardMode() while (weapon.MoveNext()) { if (weapon.Current == null) continue; + if (weapon.Current.isAPS) continue; // if (weapon.Current.GetShortName() != selectedWeapon.GetShortName()) continue; weapon.Current.autoFire = false; weapon.Current.autofireShotCount = 0; @@ -5270,7 +6298,7 @@ void GuardMode() { if (!isLegacyCMing) { - // StartCoroutine(LegacyCMRoutine()); // Depreciated + // StartCoroutine(LegacyCMRoutine()); // Deprecated } targetScanTimer -= Time.fixedDeltaTime; //advance scan timing (increased urgency) @@ -5285,9 +6313,8 @@ void GuardMode() { targetScanTimer = Time.time; - if (!guardFiringMissile) + if (!guardFiringMissile)// || (firedMissiles >= maxMissilesOnTarget && multiMissileTgtNum > 1 && BDATargetManager.TargetList(Team).Count > 1)) //grab new target, if possible { - SmartFindTarget(); if (guardTarget == null || selectedWeapon == null) @@ -5302,48 +6329,61 @@ void GuardMode() { if (selectedWeapon.GetWeaponClass() == WeaponClasses.Missile || selectedWeapon.GetWeaponClass() == WeaponClasses.SLW) { - bool launchAuthorized = true; - bool pilotAuthorized = true; - //(!pilotAI || pilotAI.GetLaunchAuthorization(guardTarget, this)); + if (CurrentMissile != null) // Reloadable rails can give a null missile. + { + bool launchAuthorized = true; + bool pilotAuthorized = true; + //(!pilotAI || pilotAI.GetLaunchAuthorization(guardTarget, this)); - float targetAngle = Vector3.Angle(-transform.forward, guardTarget.transform.position - transform.position); - float targetDistance = Vector3.Distance(currentTarget.position, transform.position); - MissileLaunchParams dlz = MissileLaunchParams.GetDynamicLaunchParams(CurrentMissile, guardTarget.Velocity(), guardTarget.CoM); + float targetAngle = Vector3.Angle(-transform.forward, guardTarget.transform.position - transform.position); + float targetDistance = Vector3.Distance(currentTarget.position, transform.position); + MissileLaunchParams dlz = MissileLaunchParams.GetDynamicLaunchParams(CurrentMissile, guardTarget.Velocity(), guardTarget.CoM, 1, (CurrentMissile.TargetingMode == MissileBase.TargetingModes.Laser + && BDATargetManager.ActiveLasers.Count <= 0 || CurrentMissile.TargetingMode == MissileBase.TargetingModes.Radar && !_radarsEnabled && !CurrentMissile.radarLOAL)); - if (targetAngle > guardAngle / 2) //dont fire yet if target out of guard angle - { - launchAuthorized = false; - } - else if (targetDistance >= dlz.maxLaunchRange || targetDistance <= dlz.minLaunchRange) //fire the missile only if target is further than missiles min launch range - { - launchAuthorized = false; - } + if (targetAngle > guardAngle / 2) //dont fire yet if target out of guard angle + { + launchAuthorized = false; + } + else if (targetDistance >= dlz.maxLaunchRange || targetDistance <= dlz.minLaunchRange) //fire the missile only if target is further than missiles min launch range + { + launchAuthorized = false; + } - // Check that launch is possible before entering GuardMissileRoutine - launchAuthorized = launchAuthorized && GetLaunchAuthorization(guardTarget, this); + // Check that launch is possible before entering GuardMissileRoutine, or that missile is on a turret + MissileLauncher ml = CurrentMissile as MissileLauncher; + launchAuthorized = launchAuthorized && (GetLaunchAuthorization(guardTarget, this) || (ml is not null && ml.missileTurret)); - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log("[BDArmory.MissileFire]: " + vessel.vesselName + " launchAuth=" + launchAuthorized + ", pilotAut=" + pilotAuthorized + ", missilesAway/Max=" + missilesAway + "/" + maxMissilesOnTarget); - if (missilesAway < maxMissilesOnTarget) - { - if (!guardFiringMissile && launchAuthorized - && (CurrentMissile != null && (CurrentMissile.TargetingMode != MissileBase.TargetingModes.Radar || (vesselRadarData != null && (!vesselRadarData.locked || vesselRadarData.lockedTargetData.vessel == guardTarget))))) // Allow firing multiple missiles at the same target. FIXME This is a stop-gap until proper multi-locking support is available. + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} launchAuth={launchAuthorized}, pilotAut={pilotAuthorized}, missilesAway/Max={firedMissiles}/{maxMissilesOnTarget}"); + + if (firedMissiles < maxMissilesOnTarget) { - StartCoroutine(GuardMissileRoutine()); + if (CurrentMissile.TargetingMode == MissileBase.TargetingModes.Radar && _radarsEnabled && !CurrentMissile.radarLOAL && MaxradarLocks < vesselRadarData.GetLockedTargets().Count) + { + launchAuthorized = false; //don't fire SARH if radar can't support the needed radar lock + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.MissileFire]: radar lock number exceeded to launch!"); + } + + if (!guardFiringMissile && launchAuthorized) + //&& (CurrentMissile.TargetingMode != MissileBase.TargetingModes.Radar || (vesselRadarData != null && (!vesselRadarData.locked || vesselRadarData.lockedTargetData.vessel == guardTarget)))) // Allow firing multiple missiles at the same target. FIXME This is a stop-gap until proper multi-locking support is available. + { + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} firing {(unguidedWeapon ? "unguided" : "")} missile"); + StartCoroutine(GuardMissileRoutine()); + } + } + else if (BDArmorySettings.DEBUG_MISSILES) + { + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} waiting for missile to be ready..."); } - } - else if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - Debug.Log("[BDArmory.MissileFire]: " + vessel.vesselName + " waiting for missile to be ready..."); - } - // if (!launchAuthorized || !pilotAuthorized || missilesAway >= maxMissilesOnTarget) - // { - // targetScanTimer -= 0.5f * targetScanInterval; - // } + // if (!launchAuthorized || !pilotAuthorized || missilesAway >= maxMissilesOnTarget) + // { + // targetScanTimer -= 0.5f * targetScanInterval; + // } + } } - else if (selectedWeapon.GetWeaponClass() == WeaponClasses.Bomb) + else if (selectedWeapon != null && selectedWeapon.GetWeaponClass() == WeaponClasses.Bomb) { if (!guardFiringMissile) { @@ -5375,15 +6415,15 @@ void GuardMode() void UpdateGuardViewScan() { - results = RadarUtils.GuardScanInDirection(this, transform, guardAngle, guardRange); + results = RadarUtils.GuardScanInDirection(this, transform, guardAngle, rwr.omniDetection ? Mathf.Max(guardRange, 50000) : Mathf.Min(guardRange, 50000)); incomingThreatVessel = null; - if (results.foundMissile) + if (results.foundMissile && (results.incomingMissiles[0].distance < guardRange || results.incomingMissiles[0].time < cmThreshold)) //RWR detects things beyond visual range, allow reaction to detected high-velocity missiles where waiting till visrange would leave very little time to react { - if (BDArmorySettings.DRAW_DEBUG_LABELS && (!missileIsIncoming || results.incomingMissiles[0].distance < 1000f)) + if (BDArmorySettings.DEBUG_AI && (!missileIsIncoming || results.incomingMissiles[0].distance < 1000f)) { foreach (var incomingMissile in results.incomingMissiles) - Debug.Log("[BDArmory.MissileFire]: " + vessel.vesselName + " incoming missile (" + incomingMissile.vessel.vesselName + " of type " + incomingMissile.guidanceType + " from " + (incomingMissile.weaponManager != null && incomingMissile.weaponManager.vessel != null ? incomingMissile.weaponManager.vessel.vesselName : "unknown") + ") found at distance " + incomingMissile.distance + "m"); + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} incoming missile ({incomingMissile.vessel.vesselName} of type {incomingMissile.guidanceType} from {(incomingMissile.weaponManager != null && incomingMissile.weaponManager.vessel != null ? incomingMissile.weaponManager.vessel.vesselName : "unknown")}) found at distance {incomingMissile.distance} m"); } missileIsIncoming = true; incomingMissileLastDetected = Time.time; @@ -5393,48 +6433,105 @@ void UpdateGuardViewScan() incomingThreatPosition = results.incomingMissiles[0].position; incomingThreatVessel = results.incomingMissiles[0].vessel; incomingMissileVessel = results.incomingMissiles[0].vessel; - if (rwr && !rwr.rwrEnabled) rwr.EnableRWR(); - if (rwr && rwr.rwrEnabled && !rwr.displayRWR) rwr.displayRWR = true; - - if (results.foundHeatMissile) + if (rwr && rwr.omniDetection) //enable omniRWRs for all incoming threats { - StartCoroutine(UnderAttackRoutine()); - - FireFlares(); + if (!rwr.rwrEnabled) rwr.EnableRWR(); + if (rwr.rwrEnabled && !rwr.displayRWR) rwr.displayRWR = true; } - - if (results.foundRadarMissile) + //radar missiles + if (results.foundRadarMissile) //have this require an RWR? { + if (rwr && !rwr.omniDetection) + { + if (!rwr.rwrEnabled) rwr.EnableRWR(); + if (rwr.rwrEnabled && !rwr.displayRWR) rwr.displayRWR = true; + } StartCoroutine(UnderAttackRoutine()); FireChaff(); FireECM(); } - - if (results.foundAGM) + //laser missiles + if (results.foundAGM) //Assume Laser Warning Reciever regardless of omniDetection? or move laser missiles to the passive missiles section? { StartCoroutine(UnderAttackRoutine()); - //do smoke CM here. + FireSmoke(); if (targetMissiles && guardTarget == null) { //targetScanTimer = Mathf.Min(targetScanInterval, Time.time - targetScanInterval + 0.5f); targetScanTimer -= targetScanInterval / 2; } } - - if (results.foundAntiRadiationMissile) + //passive missiles + if (results.foundHeatMissile || results.foundAntiRadiationMissile) { - StartCoroutine(UnderAttackRoutine()); - - // Turn off the radars - using (List.Enumerator rd = radars.GetEnumerator()) - while (rd.MoveNext()) + if (rwr && rwr.omniDetection) + { + if (results.foundHeatMissile) { - if (rd.Current != null || rd.Current.canLock) - rd.Current.DisableRadar(); + FireFlares(); + FireOCM(true); + } + if (results.foundAntiRadiationMissile) + { + using (List.Enumerator rd = radars.GetEnumerator()) + while (rd.MoveNext()) + { + if (rd.Current != null || rd.Current.canLock) + rd.Current.DisableRadar(); + } + } + StartCoroutine(UnderAttackRoutine()); + } + else //one passive missile is going to be indistinguishable from another, until it gets close enough to evaluate + { + if (vessel.LandedOrSplashed) //assume antirads against ground targets + { + if (radars.Count > 0) + { + using (List.Enumerator rd = radars.GetEnumerator()) + while (rd.MoveNext()) + { + if (rd.Current != null || rd.Current.canLock) + rd.Current.DisableRadar(); + _radarsEnabled = false; + } + } + } + else //likely a heatseeker, but could be an AA HARM... + { + if (incomingMissileDistance <= guardRange * 0.33f) //within ID range? + { + if (results.foundHeatMissile) + { + FireFlares(); + FireOCM(true); + } + else //it's an Antirad!? Uh-oh, blip radar! + { + if (radars.Count > 0) + { + using (List.Enumerator rd = radars.GetEnumerator()) + while (rd.MoveNext()) + { + if (rd.Current != null || rd.Current.canLock) + rd.Current.DisableRadar(); + _radarsEnabled = false; + } + } + } + } + else //assume heater + { + FireFlares(); + FireOCM(true); + } } + StartCoroutine(UnderAttackRoutine()); + } } + } else { @@ -5460,9 +6557,13 @@ void UpdateGuardViewScan() priorGunThreatVessel = results.threatVessel; incomingMissTime = 0f; } + if ((pilotAI != null && incomingMissTime >= pilotAI.evasionTimeThreshold && incomingMissDistance < pilotAI.evasionThreshold) || AI != null && pilotAI == null) // If we haven't been under fire long enough, ignore gunfire + { + FireOCM(false); //enable visual coutnermeasures if under fire + } if (results.threatWeaponManager != null) { - incomingMissDistance = results.missDistance; + incomingMissDistance = results.missDistance + results.missDeviation; TargetInfo nearbyFriendly = BDATargetManager.GetClosestFriendly(this); TargetInfo nearbyThreat = BDATargetManager.GetTargetFromWeaponManager(results.threatWeaponManager); @@ -5475,21 +6576,20 @@ void UpdateGuardViewScan() { SetOverrideTarget(nearbyFriendly.weaponManager.currentTarget); nearbyFriendly.weaponManager.SetOverrideTarget(nearbyThreat); - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log("[BDArmory.MissileFire]: " + vessel.vesselName + " called for help from " + nearbyFriendly.Vessel.vesselName + " and took its target in return"); + if (BDArmorySettings.DEBUG_AI) + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} called for help from {nearbyFriendly.Vessel.vesselName} and took its target in return"); //basically, swap targets to cover each other } else { //otherwise, continue engaging the current target for now nearbyFriendly.weaponManager.SetOverrideTarget(nearbyThreat); - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log("[BDArmory.MissileFire]: " + vessel.vesselName + " called for help from " + nearbyFriendly.Vessel.vesselName); + if (BDArmorySettings.DEBUG_AI) + Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} called for help from {nearbyFriendly.Vessel.vesselName}"); } } } - - StartCoroutine(UnderAttackRoutine()); + StartCoroutine(UnderAttackRoutine()); //this seems to be firing all the time, not just when bullets are flying towards craft...? StartCoroutine(UnderFireRoutine()); } else @@ -5604,6 +6704,7 @@ public void StartGuardTurretFiring() weapon.Current.targetEngines = false; weapon.Current.targetWeapons = false; weapon.Current.targetMass = false; + weapon.Current.targetRandom = false; } else { @@ -5611,6 +6712,7 @@ public void StartGuardTurretFiring() weapon.Current.targetEngines = targetEngine; weapon.Current.targetWeapons = targetWeapon; weapon.Current.targetMass = targetMass; + weapon.Current.targetRandom = targetRandom; } weapon.Current.autoFireTimer = Time.time; @@ -5647,7 +6749,7 @@ public void UpdateMaxGunRange(Vessel v) rangeEditor.maxValue = maxGunRange; if (BDArmorySetup.Instance.textNumFields != null && BDArmorySetup.Instance.textNumFields.ContainsKey("gunRange")) { BDArmorySetup.Instance.textNumFields["gunRange"].maxValue = maxGunRange; } gunRange = Mathf.Min(gunRange, maxGunRange); - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.MissileFire]: Updating gun range of " + v.vesselName + " to " + gunRange + " of " + maxGunRange); + if (BDArmorySettings.DEBUG_AI) Debug.Log($"[BDArmory.MissileFire]: Updating gun range of {v.vesselName} to {gunRange} of {maxGunRange}"); } public void UpdateMaxGunRange(Part eventPart) @@ -5670,7 +6772,7 @@ public void UpdateMaxGunRange(Part eventPart) if (gunRange == rangeEditor.maxValue) { gunRange = maxGunRange; } rangeEditor.maxValue = maxGunRange; gunRange = Mathf.Min(gunRange, maxGunRange); - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.MissileFire]: Updating gun range of " + EditorLogic.fetch.ship.shipName + " to " + gunRange + " of " + maxGunRange); + if (BDArmorySettings.DEBUG_AI) Debug.Log($"[BDArmory.MissileFire]: Updating gun range of {EditorLogic.fetch.ship.shipName} to {gunRange} of {maxGunRange}"); } public float ThreatClosingTime(Vessel threat) @@ -5678,16 +6780,13 @@ public float ThreatClosingTime(Vessel threat) float closureTime = 3600f; // Default closure time of one hour if (threat) // If we weren't passed a null { - float targetDistance = Vector3.Distance(threat.transform.position, vessel.transform.position); - Vector3 currVel = (float)vessel.srfSpeed * vessel.Velocity().normalized; - closureTime = Mathf.Clamp((float)(1 / ((threat.Velocity() - currVel).magnitude / targetDistance)), 0f, closureTime); - // Debug.Log("[BDArmory.MissileFire]: Threat from " + threat.GetDisplayName() + " is " + closureTime.ToString("0.0") + " seconds away!"); + closureTime = vessel.TimeToCPA(threat, closureTime); } return closureTime; } // moved from pilot AI, as it does not really do anything AI related? - bool GetLaunchAuthorization(Vessel targetV, MissileFire mf) + public bool GetLaunchAuthorization(Vessel targetV, MissileFire mf) { bool launchAuthorized = false; MissileBase missile = mf.CurrentMissile; @@ -5699,22 +6798,20 @@ bool GetLaunchAuthorization(Vessel targetV, MissileFire mf) target = MissileGuidance.GetAirToAirFireSolution(missile, targetV); } - float boresightFactor = (mf.vessel.LandedOrSplashed || targetV.LandedOrSplashed || missile.allAspect) ? 0.75f : 0.35f; // Allow launch at close to maxOffBoresight for ground targets or missiles with allAspect = true + float boresightFactor = (mf.vessel.LandedOrSplashed || targetV.LandedOrSplashed || missile.uncagedLock) ? 0.75f : 0.35f; // Allow launch at close to maxOffBoresight for ground targets or missiles with allAspect = true //if(missile.TargetingMode == MissileBase.TargetingModes.Gps) maxOffBoresight = 45; // Check that target is within maxOffBoresight now and in future time fTime - launchAuthorized = Vector3.Angle(missile.GetForwardTransform(), target - missile.transform.position) < missile.maxOffBoresight * boresightFactor; // Launch is possible now + launchAuthorized = Vector3.Angle(missile.GetForwardTransform(), target - missile.transform.position) < (unguidedWeapon ? 5 : missile.maxOffBoresight * boresightFactor); // Launch is possible now if (launchAuthorized) { - float fTime = 2f; + float fTime = Mathf.Min(missile.dropTime, 2f); Vector3 futurePos = target + (targetV.Velocity() * fTime); Vector3 myFuturePos = vessel.ReferenceTransform.position + (vessel.Velocity() * fTime); - launchAuthorized = launchAuthorized && (Vector3.Angle(vessel.ReferenceTransform.up, futurePos - myFuturePos) < missile.maxOffBoresight * boresightFactor); // Launch is likely also possible at fTime - + launchAuthorized = launchAuthorized && (Vector3.Angle(vessel.ReferenceTransform.up, futurePos - myFuturePos) < (unguidedWeapon ? 5 : missile.maxOffBoresight * boresightFactor)); // Launch is likely also possible at fTime } - } return launchAuthorized; @@ -5742,7 +6839,7 @@ int CheckTurret(float distance) { return 2; } - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_WEAPONS) { Debug.Log("[BDArmory.MissileFire]: Checking turrets"); } @@ -5759,45 +6856,45 @@ int CheckTurret(float distance) { if (weapon.Current.isOverheated) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_WEAPONS) { - Debug.Log("[BDArmory.MissileFire]: " + selectedWeapon + " is overheated!"); + Debug.Log($"[BDArmory.MissileFire]: {selectedWeapon} is overheated!"); } return -1; } if (weapon.Current.isReloading) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_WEAPONS) { - Debug.Log("[BDArmory.MissileFire]: " + selectedWeapon + " is reloading!"); + Debug.Log($"[BDArmory.MissileFire]: {selectedWeapon} is reloading!"); } return -1; } if (!weapon.Current.hasGunner) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_WEAPONS) { - Debug.Log("[BDArmory.MissileFire]: " + selectedWeapon + " has no gunner!"); + Debug.Log($"[BDArmory.MissileFire]: {selectedWeapon} has no gunner!"); } return -1; } if (CheckAmmo(weapon.Current) || BDArmorySettings.INFINITE_AMMO) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_WEAPONS) { - Debug.Log("[BDArmory.MissileFire]: " + selectedWeapon + " is valid!"); + Debug.Log($"[BDArmory.MissileFire]: {selectedWeapon} is valid!"); } return 1; } - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_WEAPONS) { - Debug.Log("[BDArmory.MissileFire]: " + selectedWeapon + " has no ammo."); + Debug.Log($"[BDArmory.MissileFire]: {selectedWeapon} has no ammo."); } return -1; } - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_WEAPONS) { - Debug.Log("[BDArmory.MissileFire]: " + selectedWeapon + " cannot reach target (" + distance + " vs " + weapon.Current.maxEffectiveDistance + ", yawRange: " + weapon.Current.yawRange + "). Continuing."); + Debug.Log($"[BDArmory.MissileFire]: {selectedWeapon} cannot reach target ({distance} vs {weapon.Current.maxEffectiveDistance}, yawRange: {weapon.Current.yawRange}). Continuing."); } //else return 0; } @@ -5813,7 +6910,7 @@ bool TargetInTurretRange(ModuleTurret turret, float tolerance, Vessel gTarget = if (!guardTarget) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_WEAPONS) { Debug.Log("[BDArmory.MissileFire]: Checking turret range but no guard target"); } @@ -5829,21 +6926,21 @@ bool TargetInTurretRange(ModuleTurret turret, float tolerance, Vessel gTarget = { case WeaponClasses.Gun: { - var effectiveBulletSpeed = (turret.part.rb.velocity + Krakensbane.GetFrameVelocityV3f() + weapon.bulletVelocity * direction.normalized).magnitude; + var effectiveBulletSpeed = (turret.part.rb.velocity + BDKrakensbane.FrameVelocityV3f + weapon.bulletVelocity * direction.normalized).magnitude; var timeOfFlight = direction.magnitude / effectiveBulletSpeed; direction -= 0.5f * FlightGlobals.getGeeForceAtPosition(vessel.transform.position) * timeOfFlight * timeOfFlight; break; } case WeaponClasses.Rocket: { - var effectiveRocketSpeed = (turret.part.rb.velocity + Krakensbane.GetFrameVelocityV3f() + (weapon.thrust * weapon.thrustTime / weapon.rocketMass) * direction.normalized).magnitude; + var effectiveRocketSpeed = (turret.part.rb.velocity + BDKrakensbane.FrameVelocityV3f + (weapon.thrust * weapon.thrustTime / weapon.rocketMass) * direction.normalized).magnitude; var timeOfFlight = direction.magnitude / effectiveRocketSpeed; direction -= 0.5f * FlightGlobals.getGeeForceAtPosition(vessel.transform.position) * timeOfFlight * timeOfFlight; break; } } } - Vector3 directionYaw = Vector3.ProjectOnPlane(direction, turretTransform.up); + Vector3 directionYaw = direction.ProjectOnPlanePreNormalized(turretTransform.up); float angleYaw = Vector3.Angle(turretTransform.forward, directionYaw); float signedAnglePitch = 90 - Vector3.Angle(turretTransform.up, direction); @@ -5851,17 +6948,17 @@ bool TargetInTurretRange(ModuleTurret turret, float tolerance, Vessel gTarget = if (angleYaw < (turret.yawRange / 2) + tolerance && withinPitchRange) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_WEAPONS) { - Debug.Log("[BDArmory.MissileFire]: Checking turret range - target is INSIDE gimbal limits! signedAnglePitch: " + signedAnglePitch + ", minPitch: " + turret.minPitch + ", maxPitch: " + turret.maxPitch + ", tolerance: " + tolerance); + Debug.Log($"[BDArmory.MissileFire]: Checking turret range - target is INSIDE gimbal limits! signedAnglePitch: {signedAnglePitch}, minPitch: {turret.minPitch}, maxPitch: {turret.maxPitch}, tolerance: {tolerance}"); } return true; } else { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_WEAPONS) { - Debug.Log("[BDArmory.MissileFire]: Checking turret range - target is OUTSIDE gimbal limits! signedAnglePitch: " + signedAnglePitch + ", minPitch: " + turret.minPitch + ", maxPitch: " + turret.maxPitch + ", angleYaw: " + angleYaw + ", tolerance: " + tolerance); + Debug.Log($"[BDArmory.MissileFire]: Checking turret range - target is OUTSIDE gimbal limits! signedAnglePitch: {signedAnglePitch}, minPitch: {turret.minPitch}, maxPitch: {turret.maxPitch}, angleYaw: {angleYaw}, tolerance: {tolerance}"); } return false; } @@ -5881,7 +6978,8 @@ public bool CheckAmmo(ModuleWeapon weapon) while (p.MoveNext()) { if (p.Current == null) continue; - using (IEnumerator resource = p.Current.Resources.GetEnumerator()) + // using (IEnumerator resource = p.Current.Resources.GetEnumerator()) + using (var resource = p.Current.Resources.dict.Values.GetEnumerator()) while (resource.MoveNext()) { if (resource.Current == null) continue; @@ -5896,37 +6994,61 @@ public bool CheckAmmo(ModuleWeapon weapon) } } + public bool CheckAmmo(MissileBase weapon) + { + int ammoCount = weapon.missilecount; + if (BDArmorySettings.INFINITE_ORDINANCE) //check for infinite ammo + { + return true; + } + else + { + if (ammoCount > 0) return true; + } + return false; + } + public bool outOfAmmo = false; // Indicator for being out of ammo. - public bool HasWeaponsAndAmmo(List weaponClasses = null) + public bool hasWeapons = true; // Indicator for having weapons. + public bool HasWeaponsAndAmmo() { // Check if the vessel has both weapons and ammo for them. Optionally, restrict checks to a subset of the weapon classes. - if (outOfAmmo && !BDArmorySettings.INFINITE_AMMO) return false; // It's already been checked and found to be true, don't look again. + if (!hasWeapons || (outOfAmmo && !BDArmorySettings.INFINITE_AMMO && !BDArmorySettings.INFINITE_ORDINANCE)) return false; // It's already been checked and found to be false, don't look again. bool hasWeaponsAndAmmo = false; + hasWeapons = false; foreach (var weapon in VesselModuleRegistry.GetModules(vessel)) { if (weapon == null) continue; // First entry is the "no weapon" option. - if (weaponClasses != null && !weaponClasses.Contains(weapon.GetWeaponClass())) continue; // Ignore weapon classes we're not interested in. + hasWeapons = true; if (weapon.GetWeaponClass() == WeaponClasses.Gun || weapon.GetWeaponClass() == WeaponClasses.Rocket) { if (BDArmorySettings.INFINITE_AMMO || CheckAmmo((ModuleWeapon)weapon)) { hasWeaponsAndAmmo = true; break; } // If the gun has ammo or we're using infinite ammo, return true after cleaning up. } + else if (weapon.GetWeaponClass() == WeaponClasses.Missile || weapon.GetWeaponClass() == WeaponClasses.Bomb || weapon.GetWeaponClass() == WeaponClasses.SLW) + { + if (BDArmorySettings.INFINITE_ORDINANCE || CheckAmmo((MissileBase)weapon)) { hasWeaponsAndAmmo = true; break; } // If the gun has ammo or we're using infinite ammo, return true after cleaning up. + } else { hasWeaponsAndAmmo = true; break; } // Other weapon types don't have ammo, or use electric charge, which could recharge. } outOfAmmo = !hasWeaponsAndAmmo; // Set outOfAmmo if we don't have any guns with compatible ammo. + if (BDArmorySettings.DEBUG_WEAPONS && outOfAmmo) Debug.Log($"[BDArmory.MissileFire]: {vessel.vesselName} has run out of ammo!"); return hasWeaponsAndAmmo; } - public int CountWeapons(List weaponClasses = null) + public int CountWeapons() { // Count number of weapons with ammo int countWeaponsAndAmmo = 0; foreach (var weapon in VesselModuleRegistry.GetModules(vessel)) { if (weapon == null) continue; // First entry is the "no weapon" option. - if (weaponClasses != null && !weaponClasses.Contains(weapon.GetWeaponClass())) continue; // Ignore weapon classes we're not interested in. if (weapon.GetWeaponClass() == WeaponClasses.Gun || weapon.GetWeaponClass() == WeaponClasses.Rocket || weapon.GetWeaponClass() == WeaponClasses.DefenseLaser) { if (weapon.GetShortName().EndsWith("Laser")) { countWeaponsAndAmmo++; continue; } // If it's a laser (counts as a gun) consider it as having ammo and count it, since electric charge can replenish. if (BDArmorySettings.INFINITE_AMMO || CheckAmmo((ModuleWeapon)weapon)) { countWeaponsAndAmmo++; } // If the gun has ammo or we're using infinite ammo, count it. } + else if (weapon.GetWeaponClass() == WeaponClasses.Missile || weapon.GetWeaponClass() == WeaponClasses.SLW || weapon.GetWeaponClass() == WeaponClasses.Bomb) + { + if (BDArmorySettings.INFINITE_ORDINANCE || CheckAmmo((MissileBase)weapon)) { countWeaponsAndAmmo++; } // If the gun has ammo or we're using infinite ammo, count it. + } else { countWeaponsAndAmmo++; } // Other weapon types don't have ammo, or use electric charge, which could recharge, so count them. } return countWeaponsAndAmmo; @@ -5941,22 +7063,34 @@ void ToggleTurret() if (weapon.Current == null) continue; if (selectedWeapon == null) { - if (!weapon.Current.isAPS) weapon.Current.DisableWeapon(); + if (weapon.Current.turret && guardMode) + { + weapon.Current.StandbyWeapon(); + } + else + { + weapon.Current.DisableWeapon(); + } } else if (weapon.Current.GetShortName() != selectedWeapon.GetShortName()) { if (weapon.Current.turret != null && (weapon.Current.ammoCount > 0 || BDArmorySettings.INFINITE_AMMO)) // Put turrets in standby (tracking only) mode instead of disabling them if they have ammo. { - if (!weapon.Current.isAPS) weapon.Current.StandbyWeapon(); + weapon.Current.StandbyWeapon(); } else { - if (!weapon.Current.isAPS) weapon.Current.DisableWeapon(); + weapon.Current.DisableWeapon(); } } else { weapon.Current.EnableWeapon(); + if (weapon.Current.dualModeAPS) + { + weapon.Current.isAPS = false; + weapon.Current.aiControlled = false; + } } } } @@ -6006,7 +7140,7 @@ void BombAimer() Vector3 dragForce = Vector3.zero; Vector3 prevPos = ml.MissileReferenceTransform.position; Vector3 currPos = ml.MissileReferenceTransform.position; - //Vector3 simVelocity = vessel.rb_velocity; + Vector3 closestPos = ml.MissileReferenceTransform.position; Vector3 simVelocity = vessel.Velocity(); //Issue #92 MissileLauncher launcher = ml as MissileLauncher; @@ -6030,18 +7164,44 @@ void BombAimer() bombAimerPosition = Vector3.zero; int aimerLayerMask = (int)(LayerMasks.Scenery | LayerMasks.EVA); // Why EVA? - + float ordinanceMass = launcher.part.mass; + float ordinanceThrust = launcher.cruiseThrust; + float ordinanceBoost = launcher.thrust; + float thrustTime = launcher.cruiseTime + launcher.boostTime; + Vector3 pointingDirection = launcher.MissileReferenceTransform.forward; + if (FlightGlobals.RefFrameIsRotating) + { simVelocity += 0.5f * simDeltaTime * FlightGlobals.getGeeForceAtPosition(currPos); } + simVelocity += 0.5f * ordinanceBoost / ordinanceMass * simDeltaTime * pointingDirection; bool simulating = true; + var simStartTime = Time.realtimeSinceStartup; while (simulating) { prevPos = currPos; currPos += simVelocity * simDeltaTime; + Vector3d gravity = FlightGlobals.getGeeForceAtPosition(currPos); float atmDensity = (float) FlightGlobals.getAtmDensity(FlightGlobals.getStaticPressure(currPos), FlightGlobals.getExternalTemperature(), FlightGlobals.currentMainBody); - simVelocity += FlightGlobals.getGeeForceAtPosition(currPos) * simDeltaTime; + if (FlightGlobals.RefFrameIsRotating) + { simVelocity += 0.5f * simDeltaTime * gravity; } + if (simTime <= thrustTime) + { + if (simTime < launcher.boostTime) + { + simVelocity += ordinanceBoost / ordinanceMass * simDeltaTime * pointingDirection; + } + else + { + simVelocity += ordinanceThrust / ordinanceMass * simDeltaTime * pointingDirection; + } + if (FlightGlobals.RefFrameIsRotating) + { + simVelocity += gravity * simDeltaTime; + } + } + float simSpeedSquared = simVelocity.sqrMagnitude; launcher = ml as MissileLauncher; @@ -6062,7 +7222,8 @@ void BombAimer() dragForce = (0.008f * bombPart.mass) * drag * 0.5f * simSpeedSquared * atmDensity * simVelocity.normalized; simVelocity -= (dragForce / bombPart.mass) * simDeltaTime; - + //float lift = 0.5f * atmDensity * simSpeedSquared * launcher.liftArea * BDArmorySettings.GLOBAL_LIFT_MULTIPLIER * MissileGuidance.DefaultLiftCurve.Evaluate(1); + //simVelocity += VectorUtils.GetUpDirection(currPos) * lift; Ray ray = new Ray(prevPos, currPos - prevPos); RaycastHit hitInfo; if (Physics.Raycast(ray, out hitInfo, Vector3.Distance(prevPos, currPos), aimerLayerMask)) @@ -6076,20 +7237,31 @@ void BombAimer() (FlightGlobals.getAltitudeAtPos(currPos) * FlightGlobals.getUpAxis()); simulating = false; } - + if (guardTarget) + { + float targetDist = Vector3.Distance(currPos, guardTarget.CoM) - guardTarget.GetRadius(); + if (targetDist < CurrentMissile.GetBlastRadius()) + { + bombAimerPosition = currPos; + simulating = false; + } + } + if (Time.realtimeSinceStartup - simStartTime >= 0.1f) + { + bombAimerPosition = currPos; + simulating = false; + break; + } simTime += simDeltaTime; pointPositions.Add(currPos); } //debug lines - if (BDArmorySettings.DRAW_DEBUG_LINES && BDArmorySettings.DRAW_AIMERS) + if (BDArmorySettings.DEBUG_LINES && BDArmorySettings.DRAW_AIMERS) { Vector3[] pointsArray = pointPositions.ToArray(); - LineRenderer lr = GetComponent(); - if (!lr) - { - lr = gameObject.AddComponent(); - } + lr = GetComponent(); + if (!lr) { lr = gameObject.AddComponent(); } lr.enabled = true; lr.startWidth = .1f; lr.endWidth = .1f; @@ -6099,13 +7271,18 @@ void BombAimer() lr.SetPosition(i, pointsArray[i]); } } - else - { - if (gameObject.GetComponent()) - { - gameObject.GetComponent().enabled = false; - } - } + } + + // Check GPS target is within 10m for stationary targets, and a scaling distance based on target speed for targets moving faster than ~175 m/s + bool GPSDistanceCheck() + { + return (((guardTarget.CoM - VectorUtils.GetWorldSurfacePostion(designatedGPSCoords, vessel.mainBody)).sqrMagnitude < Mathf.Max(100f, 0.004f * (float)guardTarget.srfSpeed * (float)guardTarget.srfSpeed))); + } + + // Check antiRad target is within 20m for stationary targets, and a scaling distance based on target speed for targets moving faster than ~60 m/s + bool AntiRadDistanceCheck() + { + return (VectorUtils.WorldPositionToGeoCoords(antiRadiationTarget, vessel.mainBody) - VectorUtils.WorldPositionToGeoCoords(guardTarget.CoM, vessel.mainBody)).sqrMagnitude < Mathf.Max(400f, (float)guardTarget.srfSpeed * 3.5f); } bool AltitudeTrigger() diff --git a/BDArmory/Modules/ModuleWingCommander.cs b/BDArmory/Control/ModuleWingCommander.cs similarity index 88% rename from BDArmory/Modules/ModuleWingCommander.cs rename to BDArmory/Control/ModuleWingCommander.cs index a33ceb064..02c69fa77 100644 --- a/BDArmory/Modules/ModuleWingCommander.cs +++ b/BDArmory/Control/ModuleWingCommander.cs @@ -1,16 +1,16 @@ using System; using System.Collections; using System.Collections.Generic; -using BDArmory.Core; -using BDArmory.Control; -using BDArmory.Misc; -using BDArmory.Parts; -using BDArmory.UI; using UniLinq; using UnityEngine; using KSP.Localization; -namespace BDArmory.Modules +using BDArmory.Competition; +using BDArmory.Targeting; +using BDArmory.UI; +using BDArmory.Utils; + +namespace BDArmory.Control { public class ModuleWingCommander : PartModule { @@ -102,14 +102,11 @@ IEnumerator StartupRoutine() void OnDestroy() { - if (HighLogic.LoadedSceneIsFlight) - { - GameEvents.onGameStateSave.Remove(SaveWingmen); - GameEvents.onVesselLoaded.Remove(OnVesselLoaded); - GameEvents.onVesselDestroy.Remove(OnVesselLoaded); - GameEvents.onVesselGoOnRails.Remove(OnVesselLoaded); - MissileFire.OnChangeTeam -= OnToggleTeam; - } + GameEvents.onGameStateSave.Remove(SaveWingmen); + GameEvents.onVesselLoaded.Remove(OnVesselLoaded); + GameEvents.onVesselDestroy.Remove(OnVesselLoaded); + GameEvents.onVesselGoOnRails.Remove(OnVesselLoaded); + MissileFire.OnChangeTeam -= OnToggleTeam; } void OnVesselLoaded(Vessel v) @@ -248,8 +245,10 @@ void OnGUI() } // this Rect initialization ensures any save issues with height or width of the window are resolved //BDArmorySetup.WindowRectWingCommander = new Rect(BDArmorySetup.WindowRectWingCommander.x, BDArmorySetup.WindowRectWingCommander.y, windowWidth, windowHeight); - BDArmorySetup.WindowRectWingCommander = GUI.Window(1293293, BDArmorySetup.WindowRectWingCommander, WingmenWindow, Localizer.Format("#LOC_BDArmory_WingCommander_Title"),//"WingCommander" + BDArmorySetup.SetGUIOpacity(); + BDArmorySetup.WindowRectWingCommander = GUI.Window(1293293, BDArmorySetup.WindowRectWingCommander, WingmenWindow, StringUtils.Localize("#LOC_BDArmory_WingCommander_Title"),//"WingCommander" BDArmorySetup.BDGuiSkin.window); + BDArmorySetup.SetGUIOpacity(false); if (showAGWindow) AGWindow(); } @@ -259,10 +258,10 @@ void OnGUI() List.Enumerator comPos = commandedPositions.GetEnumerator(); while (comPos.MoveNext()) { - BDGUIUtils.DrawTextureOnWorldPos(comPos.Current.worldPos, BDArmorySetup.Instance.greenDiamondTexture, + GUIUtils.DrawTextureOnWorldPos(comPos.Current.worldPos, BDArmorySetup.Instance.greenDiamondTexture, new Vector2(diamondSize, diamondSize), 0); Vector2 labelPos; - if (!BDGUIUtils.WorldToGUIPos(comPos.Current.worldPos, out labelPos)) continue; + if (!GUIUtils.WorldToGUIPos(comPos.Current.worldPos, out labelPos)) continue; labelPos.x += diamondSize / 2; labelPos.y -= 10; GUI.Label(new Rect(labelPos.x, labelPos.y, 300, 20), comPos.Current.name); @@ -305,33 +304,33 @@ void WingmenWindow(int windowID) //command buttons float commandButtonLine = 0; - CommandButton(SelectAll, Localizer.Format("#LOC_BDArmory_WingCommander_SelectAll"), ref commandButtonLine, false, false);//"Select All" + CommandButton(SelectAll, StringUtils.Localize("#LOC_BDArmory_WingCommander_SelectAll"), ref commandButtonLine, false, false);//"Select All" //commandButtonLine += 0.25f; commandSelf = GUI.Toggle( new Rect(margin, margin + buttonEndY + (commandButtonLine * (buttonHeight + buttonGap)), buttonWidth, - buttonHeight), commandSelf, Localizer.Format("#LOC_BDArmory_WingCommander_CommandSelf"), BDArmorySetup.BDGuiSkin.toggle);//"Command Self" + buttonHeight), commandSelf, StringUtils.Localize("#LOC_BDArmory_WingCommander_CommandSelf"), BDArmorySetup.BDGuiSkin.toggle);//"Command Self" commandButtonLine++; commandButtonLine += 0.10f; - CommandButton(CommandFollow, Localizer.Format("#LOC_BDArmory_WingCommander_Follow"), ref commandButtonLine, true, false);//"Follow" - CommandButton(CommandFlyTo, Localizer.Format("#LOC_BDArmory_WingCommander_FlyToPos"), ref commandButtonLine, true, waitingForFlytoPos);//"Fly To Pos" - CommandButton(CommandAttack, Localizer.Format("#LOC_BDArmory_WingCommander_AttackPos"), ref commandButtonLine, true, waitingForAttackPos);//"Attack Pos" - CommandButton(OpenAGWindow, Localizer.Format("#LOC_BDArmory_WingCommander_ActionGroup"), ref commandButtonLine, false, showAGWindow);//"Action Group" - CommandButton(CommandTakeOff, Localizer.Format("#LOC_BDArmory_WingCommander_TakeOff"), ref commandButtonLine, true, false);//"Take Off" + CommandButton(CommandFollow, StringUtils.Localize("#LOC_BDArmory_WingCommander_Follow"), ref commandButtonLine, true, false);//"Follow" + CommandButton(CommandFlyTo, StringUtils.Localize("#LOC_BDArmory_WingCommander_FlyToPos"), ref commandButtonLine, true, waitingForFlytoPos);//"Fly To Pos" + CommandButton(CommandAttack, StringUtils.Localize("#LOC_BDArmory_WingCommander_AttackPos"), ref commandButtonLine, true, waitingForAttackPos);//"Attack Pos" + CommandButton(OpenAGWindow, StringUtils.Localize("#LOC_BDArmory_WingCommander_ActionGroup"), ref commandButtonLine, false, showAGWindow);//"Action Group" + CommandButton(CommandTakeOff, StringUtils.Localize("#LOC_BDArmory_WingCommander_TakeOff"), ref commandButtonLine, true, false);//"Take Off" commandButtonLine += 0.5f; - CommandButton(CommandRelease, Localizer.Format("#LOC_BDArmory_WingCommander_Release"), ref commandButtonLine, true, false);//"Release" + CommandButton(CommandRelease, StringUtils.Localize("#LOC_BDArmory_WingCommander_Release"), ref commandButtonLine, true, false);//"Release" commandButtonLine += 0.5f; GUI.Label( new Rect(margin, buttonEndY + margin + (commandButtonLine * (buttonHeight + buttonGap)), buttonWidth, 20), - Localizer.Format("#LOC_BDArmory_WingCommander_FormationSettings") + ":", BDArmorySetup.BDGuiSkin.label);//Formation Settings + StringUtils.Localize("#LOC_BDArmory_WingCommander_FormationSettings") + ":", BDArmorySetup.BDGuiSkin.label);//Formation Settings commandButtonLine++; GUI.Label( new Rect(margin, buttonEndY + margin + (commandButtonLine * (buttonHeight + buttonGap)), buttonWidth / 3, 20), - Localizer.Format("#LOC_BDArmory_WingCommander_Spread") + ": " + spread.ToString("0"), BDArmorySetup.BDGuiSkin.label);//Spread + StringUtils.Localize("#LOC_BDArmory_WingCommander_Spread") + ": " + spread.ToString("0"), BDArmorySetup.BDGuiSkin.label);//Spread spread = GUI.HorizontalSlider( new Rect(margin + (buttonWidth / 3), @@ -340,7 +339,7 @@ void WingmenWindow(int windowID) commandButtonLine++; GUI.Label( new Rect(margin, buttonEndY + margin + (commandButtonLine * (buttonHeight + buttonGap)), buttonWidth / 3, 20), - Localizer.Format("#LOC_BDArmory_WingCommander_Lag") + ": " + lag.ToString("0"), BDArmorySetup.BDGuiSkin.label);//Lag + StringUtils.Localize("#LOC_BDArmory_WingCommander_Lag") + ": " + lag.ToString("0"), BDArmorySetup.BDGuiSkin.label);//Lag lag = GUI.HorizontalSlider( new Rect(margin + (buttonWidth / 3), @@ -352,7 +351,7 @@ void WingmenWindow(int windowID) height += ((commandButtonLine - 1) * (buttonHeight + buttonGap)); BDArmorySetup.WindowRectWingCommander.height = height; GUI.DragWindow(BDArmorySetup.WindowRectWingCommander); - BDGUIUtils.RepositionWindow(ref BDArmorySetup.WindowRectWingCommander); + GUIUtils.RepositionWindow(ref BDArmorySetup.WindowRectWingCommander); } void WingmanButton(int index, out float buttonEndY) @@ -476,7 +475,7 @@ void AGWindow() newHeight += agMargin; GUIStyle titleStyle = new GUIStyle(BDArmorySetup.BDGuiSkin.label); titleStyle.alignment = TextAnchor.MiddleCenter; - GUI.Label(new Rect(agMargin, 5, width - (2 * agMargin), 20), Localizer.Format("#LOC_BDArmory_WingCommander_ActionGroups"), titleStyle);//"Action Groups" + GUI.Label(new Rect(agMargin, 5, width - (2 * agMargin), 20), StringUtils.Localize("#LOC_BDArmory_WingCommander_ActionGroups"), titleStyle);//"Action Groups" newHeight += 20; float startButtonY = newHeight; float buttonLine = 0; @@ -528,7 +527,7 @@ IEnumerator CommandPosition(IBDAIControl wingman, PilotCommands command) yield break; } - DisplayScreenMessage(Localizer.Format("#LOC_BDArmory_WingCommander_ScreenMessage"));//"Select target coordinates.\nRight-click to cancel." + DisplayScreenMessage(StringUtils.Localize("#LOC_BDArmory_WingCommander_ScreenMessage"));//"Select target coordinates.\nRight-click to cancel." if (command == PilotCommands.FlyTo) { diff --git a/BDArmory/Control/_description b/BDArmory/Control/_description index e6aaf421d..9671420e1 100644 --- a/BDArmory/Control/_description +++ b/BDArmory/Control/_description @@ -1,2 +1,2 @@ Control logic for vessels. -FIXME AI and WM modules should probably be moved here along with various other modules, e.g., WingCommander. \ No newline at end of file +FIXME AI and WM modules should probably be moved here along with various other modules, e.g., WingCommander. diff --git a/BDArmory/CounterMeasure/CMChaff.cs b/BDArmory/CounterMeasure/CMChaff.cs index dae08f1cb..aba635145 100644 --- a/BDArmory/CounterMeasure/CMChaff.cs +++ b/BDArmory/CounterMeasure/CMChaff.cs @@ -1,7 +1,8 @@ using System.Collections; -using BDArmory.Misc; using UnityEngine; +using BDArmory.Utils; + namespace BDArmory.CounterMeasure { public class CMChaff : MonoBehaviour @@ -51,18 +52,21 @@ IEnumerator LifeRoutine() pe.EmitParticle(); float startTime = Time.time; + var wait = new WaitForFixedUpdate(); + Vector3 position; // Optimisation: avoid getting/setting transform.position more than necessary. while (Time.time - startTime < pe.maxEnergy) { - transform.position = body.GetWorldSurfacePosition(geoPos.x, geoPos.y, geoPos.z); - velocity += FlightGlobals.getGeeForceAtPosition(transform.position) * Time.fixedDeltaTime; + position = body.GetWorldSurfacePosition(geoPos.x, geoPos.y, geoPos.z); + velocity += FlightGlobals.getGeeForceAtPosition(position) * Time.fixedDeltaTime; Vector3 dragForce = (0.008f) * drag * 0.5f * velocity.sqrMagnitude * (float) - FlightGlobals.getAtmDensity(FlightGlobals.getStaticPressure(transform.position), + FlightGlobals.getAtmDensity(FlightGlobals.getStaticPressure(position), FlightGlobals.getExternalTemperature(), body) * velocity.normalized; velocity -= (dragForce) * Time.fixedDeltaTime; - transform.position += velocity * Time.fixedDeltaTime; - geoPos = VectorUtils.WorldPositionToGeoCoords(transform.position, body); - yield return new WaitForFixedUpdate(); + position += velocity * Time.fixedDeltaTime; + transform.position = position; + geoPos = VectorUtils.WorldPositionToGeoCoords(position, body); + yield return wait; } gameObject.SetActive(false); diff --git a/BDArmory/CounterMeasure/CMDropper.cs b/BDArmory/CounterMeasure/CMDropper.cs index 316a57b9b..e6dc777ac 100644 --- a/BDArmory/CounterMeasure/CMDropper.cs +++ b/BDArmory/CounterMeasure/CMDropper.cs @@ -2,12 +2,13 @@ using System.Collections; using System.Collections.Generic; using System.Text; -using BDArmory.Core; -using BDArmory.Misc; -using BDArmory.UI; using UniLinq; using UnityEngine; +using BDArmory.Settings; +using BDArmory.UI; +using BDArmory.Utils; + namespace BDArmory.CounterMeasure { public class CMDropper : PartModule @@ -18,9 +19,9 @@ public class CMDropper : PartModule public enum CountermeasureTypes { - Flare, - Chaff, - Smoke + Flare = 1 << 0, + Chaff = 1 << 1, + Smoke = 1 << 2 } public CountermeasureTypes cmType = CountermeasureTypes.Flare; @@ -30,6 +31,11 @@ public enum CountermeasureTypes UI_FloatRange(controlEnabled = true, scene = UI_Scene.Editor, minValue = 1f, maxValue = 200f, stepIncrement = 1f)] public float ejectVelocity = 30; + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_FiringPriority"), // Selection Priority + UI_FloatRange(controlEnabled = true, scene = UI_Scene.Editor, minValue = 0f, maxValue = 10f, stepIncrement = 1f)] + public float priority = 0; + public int Priority => (int)priority; + [KSPField] public string ejectTransformName; Transform ejectTransform; @@ -44,29 +50,28 @@ public enum CountermeasureTypes VesselChaffInfo vci; - [KSPAction("Fire Countermeasure")] + [KSPAction("#LOC_BDArmory_FireCountermeasure")] public void AGDropCM(KSPActionParam param) { DropCM(); } [KSPEvent(guiActive = true, guiName = "#LOC_BDArmory_FireCountermeasure", active = true)]//Fire Countermeasure - public void DropCM() + public void EventDropCM() => DropCM(); + public bool DropCM() { switch (cmType) { case CountermeasureTypes.Flare: - DropFlare(); - break; + return DropFlare(); case CountermeasureTypes.Chaff: - DropChaff(); - break; + return DropChaff(); case CountermeasureTypes.Smoke: - PopSmoke(); - break; + return PopSmoke(); } + return false; } public override void OnStart(StartState state) @@ -176,7 +181,7 @@ void SetupCM() { case "flare": cmType = CountermeasureTypes.Flare; - cmSound = GameDatabase.Instance.GetAudioClip("BDArmory/Sounds/flareSound"); + cmSound = SoundUtils.GetAudioClip("BDArmory/Sounds/flareSound"); if (!flarePool) { SetupFlarePool(); @@ -186,7 +191,7 @@ void SetupCM() case "chaff": cmType = CountermeasureTypes.Chaff; - cmSound = GameDatabase.Instance.GetAudioClip("BDArmory/Sounds/smokeEject"); + cmSound = SoundUtils.GetAudioClip("BDArmory/Sounds/smokeEject"); resourceName = "CMChaff"; vci = vessel.gameObject.GetComponent(); if (!vci) @@ -201,8 +206,8 @@ void SetupCM() case "smoke": cmType = CountermeasureTypes.Smoke; - cmSound = GameDatabase.Instance.GetAudioClip("BDArmory/Sounds/smokeEject"); - smokePoofSound = GameDatabase.Instance.GetAudioClip("BDArmory/Sounds/smokePoof"); + cmSound = SoundUtils.GetAudioClip("BDArmory/Sounds/smokeEject"); + smokePoofSound = SoundUtils.GetAudioClip("BDArmory/Sounds/smokePoof"); resourceName = "CMSmoke"; if (smokePool == null) { @@ -212,10 +217,10 @@ void SetupCM() } } - void DropFlare() + bool DropFlare() { PartResource cmResource = GetCMResource(); - if (cmResource == null || !(cmResource.amount >= 1)) return; + if (cmResource == null || !(cmResource.amount >= 1)) return false; cmResource.amount--; audioSource.pitch = UnityEngine.Random.Range(0.9f, 1.1f); audioSource.PlayOneShot(cmSound); @@ -224,7 +229,7 @@ void DropFlare() cm.transform.position = transform.position; CMFlare cmf = cm.GetComponent(); cmf.velocity = part.rb.velocity - + Krakensbane.GetFrameVelocityV3f() + + BDKrakensbane.FrameVelocityV3f + (ejectVelocity * transform.up) + (UnityEngine.Random.Range(-3f, 3f) * transform.forward) + (UnityEngine.Random.Range(-3f, 3f) * transform.right); @@ -233,12 +238,13 @@ void DropFlare() cm.SetActive(true); FireParticleEffects(); + return true; } - void DropChaff() + bool DropChaff() { PartResource cmResource = GetCMResource(); - if (cmResource == null || !(cmResource.amount >= 1)) return; + if (cmResource == null || !(cmResource.amount >= 1)) return false; cmResource.amount--; audioSource.pitch = UnityEngine.Random.Range(0.9f, 1.1f); audioSource.PlayOneShot(cmSound); @@ -254,9 +260,10 @@ void DropChaff() chaff.Emit(ejectTransform.position, ejectVelocity * ejectTransform.forward); FireParticleEffects(); + return true; } - void PopSmoke() + bool PopSmoke() { PartResource smokeResource = GetCMResource(); if (smokeResource.amount >= 1) @@ -268,12 +275,14 @@ void PopSmoke() StartCoroutine(SmokeRoutine()); FireParticleEffects(); + return true; } + return false; } IEnumerator SmokeRoutine() { - yield return new WaitForSeconds(0.2f); + yield return new WaitForSecondsFixed(0.2f); GameObject smokeCMObject = smokePool.GetPooledObject(); CMSmoke smoke = smokeCMObject.GetComponent(); smoke.velocity = part.rb.velocity + (ejectVelocity * transform.up) + @@ -292,7 +301,7 @@ IEnumerator SmokeRoutine() } audioSource.PlayOneShot(smokePoofSound); - yield return new WaitForSeconds(longestLife); + yield return new WaitForSecondsFixed(longestLife); smokeCMObject.SetActive(false); } diff --git a/BDArmory/CounterMeasure/CMFlare.cs b/BDArmory/CounterMeasure/CMFlare.cs index fb75c5a38..69257957d 100644 --- a/BDArmory/CounterMeasure/CMFlare.cs +++ b/BDArmory/CounterMeasure/CMFlare.cs @@ -1,18 +1,18 @@ using System; using System.Collections.Generic; -using BDArmory.Core; -using BDArmory.FX; -using BDArmory.Misc; -using BDArmory.UI; using UniLinq; using UnityEngine; +using BDArmory.FX; +using BDArmory.Settings; +using BDArmory.UI; +using BDArmory.Utils; + namespace BDArmory.CounterMeasure { public class CMFlare : MonoBehaviour { - List pEmitters; // = new List(); // pEmitter2 * 2 + pEmitter KSPParticleEmitter per flare. - List gaplessEmitters; // = new List(); + List pEmitters; Light[] lights; float startTime; @@ -23,7 +23,8 @@ public class CMFlare : MonoBehaviour public Vector3 velocity; - public float thermal; //heat value + public Tuple thermalSig; //heat value + public float thermal; float minThermal; float startThermal; @@ -47,80 +48,57 @@ public void SetThermal(Vessel sourceVessel) thermal *= UnityEngine.Random.Range(thermalMinMult, thermalMaxMult); - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_LABELS) Debug.Log("[BDArmory.CMFlare]: New flare generated from " + sourceVessel.GetDisplayName() + ":" + BDATargetManager.GetVesselHeatSignature(sourceVessel).ToString("0.0") + ", heat: " + thermal.ToString("0.0") + " mult: " + thermalMinMult + "-" + thermalMaxMult); */ // NEW (1.10 and later): generate flare within spectrum of emitting vessel's heat signature, but narrow range for low heats - thermal = BDATargetManager.GetVesselHeatSignature(sourceVessel); + thermalSig = BDATargetManager.GetVesselHeatSignature(sourceVessel, Vector3.zero); //if enabling heatSig occlusion in IR missiles the thermal value of flares will have to be adjusted to compensate. + thermal = thermalSig.Item1; + //Then again, these are being ejected in a range of temps, which should cover potential differences in heatreturn from a target based on occlusion. Have vector3.Zero replaced with missile position to sim occlusion level missile owuld see and set flare temps accordingly? // float minMult = Mathf.Clamp(-0.265f * Mathf.Log(sourceHeat) + 2.3f, 0.65f, 0.8f); float thermalMinMult = Mathf.Clamp(((0.00093f * thermal * thermal - 1.4457f * thermal + 1141.95f) / 1000f), 0.65f, 0.8f); // Equivalent to above, but uses polynomial for speed thermal *= UnityEngine.Random.Range(thermalMinMult, Mathf.Max(BDArmorySettings.FLARE_FACTOR, 0f) - thermalMinMult + 0.8f); - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log("[BDArmory.CMFlare]: New flare generated from " + sourceVessel.GetDisplayName() + ":" + BDATargetManager.GetVesselHeatSignature(sourceVessel).ToString("0.0") + ", heat: " + thermal.ToString("0.0")); + if (BDArmorySettings.DEBUG_OTHER) + Debug.Log("[BDArmory.CMFlare]: New flare generated from " + sourceVessel.GetDisplayName() + ":" + thermalSig.Item1.ToString("0.0") + ", heat: " + thermal.ToString("0.0")); } void OnEnable() { startThermal = thermal; minThermal = startThermal * 0.34f; // 0.3 is original value, but doesn't work well for Tigers, 0.4f gives decent performance for Tigers, 0.65 decay gives best flare performance overall based on some monte carlo analysis - - if (gaplessEmitters == null || pEmitters == null) + if (pEmitters == null) { - gaplessEmitters = new List(); - pEmitters = new List(); using (var pe = gameObject.GetComponentsInChildren().Cast().GetEnumerator()) while (pe.MoveNext()) { if (pe.Current == null) continue; - if (pe.Current.useWorldSpace) - { - BDAGaplessParticleEmitter gpe = pe.Current.gameObject.AddComponent(); - gaplessEmitters.Add(gpe); - gpe.emit = true; - } - else { EffectBehaviour.AddParticleEmitter(pe.Current); pEmitters.Add(pe.Current); - pe.Current.emit = true; } } } - List.Enumerator gEmitter = gaplessEmitters.GetEnumerator(); - while (gEmitter.MoveNext()) - { - if (gEmitter.Current == null) continue; - gEmitter.Current.emit = true; - } - gEmitter.Dispose(); - List.Enumerator pEmitter = pEmitters.GetEnumerator(); - while (pEmitter.MoveNext()) - { - if (pEmitter.Current == null) continue; - pEmitter.Current.emit = true; - } - pEmitter.Dispose(); + EnableEmitters(); - BDArmorySetup.numberOfParticleEmitters++; + ++BDArmorySetup.numberOfParticleEmitters; if (lights == null) { lights = gameObject.GetComponentsInChildren(); } - IEnumerator lgt = lights.AsEnumerable().GetEnumerator(); - while (lgt.MoveNext()) - { - if (lgt.Current == null) continue; - lgt.Current.enabled = true; - } - lgt.Dispose(); + using (IEnumerator lgt = lights.AsEnumerable().GetEnumerator()) + while (lgt.MoveNext()) + { + if (lgt.Current == null) continue; + lgt.Current.enabled = true; + } startTime = Time.time; //ksp force applier @@ -129,6 +107,8 @@ void OnEnable() BDArmorySetup.Flares.Add(this); upDirection = VectorUtils.GetUpDirection(transform.position); + + this.transform.localScale = Vector3.one; } void FixedUpdate() @@ -139,14 +119,14 @@ void FixedUpdate() } //floating origin and velocity offloading corrections - if (!FloatingOrigin.Offset.IsZero() || !Krakensbane.GetFrameVelocity().IsZero()) + if (BDKrakensbane.IsActive) { - transform.position -= FloatingOrigin.OffsetNonKrakensbane; + transform.localPosition -= BDKrakensbane.FloatingOriginOffsetNonKrakensbane; } if (velocity != Vector3.zero) { - transform.rotation = Quaternion.LookRotation(velocity, upDirection); + transform.localRotation = Quaternion.LookRotation(velocity, upDirection); } //Particle effects @@ -154,33 +134,31 @@ void FixedUpdate() Vector3 downForce = (Mathf.Clamp(velocity.magnitude, 0.1f, 150) / 150) * 20 * -upDirection; //turbulence - List.Enumerator gEmitter = gaplessEmitters.GetEnumerator(); - while (gEmitter.MoveNext()) - { - if (gEmitter.Current == null) continue; - if (!gEmitter.Current.pEmitter) continue; - try + using (var pEmitter = pEmitters.GetEnumerator()) + while (pEmitter.MoveNext()) { - gEmitter.Current.pEmitter.worldVelocity = 2 * ParticleTurbulence.flareTurbulence + downForce; - } - catch (NullReferenceException e) - { - Debug.LogWarning("[BDArmory.CMFlare]: NRE setting worldVelocity: " + e.Message); - } + if (pEmitter.Current == null) continue; + try + { + pEmitter.Current.worldVelocity = 2 * ParticleTurbulence.flareTurbulence + downForce; + } + catch (NullReferenceException e) + { + Debug.LogWarning("[BDArmory.CMFlare]: NRE setting worldVelocity: " + e.Message); + } - try - { - if (FlightGlobals.ActiveVessel && FlightGlobals.ActiveVessel.atmDensity <= 0) + try { - gEmitter.Current.emit = false; + if (FlightGlobals.ActiveVessel && FlightGlobals.ActiveVessel.atmDensity <= 0) + { + pEmitter.Current.emit = false; + } + } + catch (NullReferenceException e) + { + Debug.LogWarning("[BDArmory.CMFlare]: NRE checking density: " + e.Message); } } - catch (NullReferenceException e) - { - Debug.LogWarning("[BDArmory.CMFlare]: NRE checking density: " + e.Message); - } - } - gEmitter.Dispose(); // //thermal decay @@ -191,35 +169,24 @@ void FixedUpdate() { alive = false; BDArmorySetup.Flares.Remove(this); - - List.Enumerator pe = pEmitters.GetEnumerator(); - while (pe.MoveNext()) - { - if (pe.Current == null) continue; - pe.Current.emit = false; - } - pe.Dispose(); - - List.Enumerator gpe = gaplessEmitters.GetEnumerator(); - while (gpe.MoveNext()) - { - if (gpe.Current == null) continue; - gpe.Current.emit = false; - } - gpe.Dispose(); - - IEnumerator lgt = lights.AsEnumerable().GetEnumerator(); - while (lgt.MoveNext()) - { - if (lgt.Current == null) continue; - lgt.Current.enabled = false; - } - lgt.Dispose(); + transform.localScale = Vector3.zero; + using (var pe = pEmitters.GetEnumerator()) + while (pe.MoveNext()) + { + if (pe.Current == null) continue; + pe.Current.emit = false; + } + using (var lgt = lights.AsEnumerable().GetEnumerator()) + while (lgt.MoveNext()) + { + if (lgt.Current == null) continue; + lgt.Current.enabled = false; + } } if (Time.time - startTime > lifeTime + 11) //disable object after x seconds { - BDArmorySetup.numberOfParticleEmitters--; + --BDArmorySetup.numberOfParticleEmitters; gameObject.SetActive(false); return; } @@ -241,9 +208,21 @@ void FixedUpdate() //gravity if (FlightGlobals.RefFrameIsRotating) - velocity += FlightGlobals.getGeeForceAtPosition(transform.position) * Time.fixedDeltaTime; + velocity += FlightGlobals.getGeeForceAtPosition(currPos) * Time.fixedDeltaTime; + + transform.localPosition += velocity * Time.fixedDeltaTime; + } - transform.position += velocity * Time.fixedDeltaTime; + public void EnableEmitters() + { + if (pEmitters == null) return; + using (var emitter = pEmitters.GetEnumerator()) + while (emitter.MoveNext()) + { + if (emitter.Current == null) continue; + if (emitter.Current.name == "pEmitter") emitter.Current.emit = BDArmorySettings.FLARE_SMOKE; + else emitter.Current.emit = true; + } } } } \ No newline at end of file diff --git a/BDArmory/CounterMeasure/CMSmoke.cs b/BDArmory/CounterMeasure/CMSmoke.cs index 21186cd9f..16897f21f 100644 --- a/BDArmory/CounterMeasure/CMSmoke.cs +++ b/BDArmory/CounterMeasure/CMSmoke.cs @@ -1,6 +1,8 @@ using System.Collections; using UnityEngine; +using BDArmory.Utils; + namespace BDArmory.CounterMeasure { public class CMSmoke : MonoBehaviour @@ -14,7 +16,7 @@ void OnEnable() IEnumerator SmokeRoutine() { - yield return new WaitForSeconds(10); + yield return new WaitForSecondsFixed(10); gameObject.SetActive(false); } diff --git a/BDArmory/CounterMeasure/ModuleCloakingDevice.cs b/BDArmory/CounterMeasure/ModuleCloakingDevice.cs new file mode 100644 index 000000000..39e12d1d0 --- /dev/null +++ b/BDArmory/CounterMeasure/ModuleCloakingDevice.cs @@ -0,0 +1,286 @@ +using BDArmory.UI; +using System.Collections; +using System.Text; +using UnityEngine; + +namespace BDArmory.CounterMeasure +{ + public class ModuleCloakingDevice : PartModule + { + Coroutine cloakRoutine; + Coroutine decloakRoutine; + + [KSPField] public bool OpticalCloaking = true; + + [KSPField] public bool ThermalCloaking = false; + + [KSPField] public float opticalReductionFactor = 0.05f; //for Optic camo to reduce enemy view range + + [KSPField] public float thermalReductionFactor = 1f; //for thermoptic camo to reduce apparent thermal sig + + [KSPField] public double resourceDrain = 5; + + [KSPField] public string resourceName = "ElectricCharge"; + + [KSPField] public float CloakTime = 1; + + [KSPField] public bool alwaysOn = false; + + [KSPField] public float cooldownInterval = -1; + + [KSPField(isPersistant = true, guiActive = true, guiName = "#LOC_BDArmory_Enabled")]//Enabled + public bool cloakEnabled = false; + + bool enabling = false; + + bool disabling = false; + + float cloakTimer = 0; + + float cooldownTimer = 0; + + private BDStagingAreaGauge gauge; + + private int resourceID; + + //part anim support? + + VesselCloakInfo vesselCloak; + + [KSPAction("Enable")] + public void AGEnable(KSPActionParam param) + { + if (!cloakEnabled) + { + EnableCloak(); + } + } + + [KSPAction("Disable")] + public void AGDisable(KSPActionParam param) + { + if (cloakEnabled) + { + DisableCloak(); + } + } + + [KSPAction("Toggle")] + public void AGToggle(KSPActionParam param) + { + Toggle(); + } + + [KSPEvent(guiActiveEditor = false, guiActive = true, guiName = "#LOC_BDArmory_Toggle")]//Toggle + public void Toggle() + { + if (cloakEnabled) + { + DisableCloak(); + } + else + { + EnableCloak(); + } + } + void Start() + { + resourceID = PartResourceLibrary.Instance.GetDefinition(resourceName).id; + } + public override void OnStart(StartState state) + { + base.OnStart(state); + if (!HighLogic.LoadedSceneIsFlight) return; + part.force_activate(); + + gauge = (BDStagingAreaGauge)part.AddModule("BDStagingAreaGauge"); + GameEvents.onVesselCreate.Add(OnVesselCreate); + EnsureVesselCloak(); + } + + void OnDestroy() + { + GameEvents.onVesselCreate.Remove(OnVesselCreate); + cloakEnabled = false; + using (var Part = this.part.vessel.Parts.GetEnumerator()) + while (Part.MoveNext()) + { + if (Part.Current == null) continue; + Part.Current.SetOpacity(1); + } + } + + void OnVesselCreate(Vessel v) + { + if (v == vessel) + EnsureVesselCloak(); + } + + public void EnableCloak() + { + if (enabling || cloakEnabled) return; + if (cooldownTimer > 0) return; + EnsureVesselCloak(); + + StopCloakDecloakRoutines(); + cloakTimer = 0; + cloakRoutine = StartCoroutine(CloakRoutine()); + } + + public void DisableCloak() + { + if (disabling || !cloakEnabled) return; + EnsureVesselCloak(); + + vesselCloak.RemoveCloak(this); + cloakEnabled = false; + + StopCloakDecloakRoutines(); + cloakTimer = CloakTime; + decloakRoutine = StartCoroutine(DecloakRoutine()); + } + + void StopCloakDecloakRoutines() + { + if (decloakRoutine != null) + { + StopCoroutine(DecloakRoutine()); + decloakRoutine = null; + } + + if (cloakRoutine != null) + { + StopCoroutine(CloakRoutine()); + cloakRoutine = null; + } + } + + public override void OnFixedUpdate() + { + base.OnFixedUpdate(); + + if (alwaysOn && !cloakEnabled) + { + EnableCloak(); + } + + if (cloakEnabled) + { + EnsureVesselCloak(); + + DrainElectricity(); + } + } + + void EnsureVesselCloak() + { + if (!vesselCloak || vesselCloak.vessel != vessel) + { + vesselCloak = vessel.gameObject.GetComponent(); + if (!vesselCloak) + { + vesselCloak = vessel.gameObject.AddComponent(); + } + } + + vesselCloak.DelayedCleanCloakList(); + } + + void DrainElectricity() + { + if (resourceDrain <= 0) + { + return; + } + + double drainAmount = resourceDrain * TimeWarp.fixedDeltaTime; + double chargeAvailable = part.RequestResource(resourceID, drainAmount, ResourceFlowMode.ALL_VESSEL); + if (chargeAvailable < drainAmount * 0.95f) + { + DisableCloak(); + } + //look into having cost scale with vessel size? + } + + IEnumerator CloakRoutine() + { + var wait = new WaitForFixedUpdate(); + enabling = true; + while (cloakTimer < CloakTime) + { + yield return wait; + } + enabling = false; + vesselCloak.AddCloak(this); + cloakEnabled = true; + } + + IEnumerator DecloakRoutine() + { + var wait = new WaitForFixedUpdate(); + disabling = true; + while (cloakTimer > 0) + { + yield return wait; + } + disabling = false; + cooldownTimer = cooldownInterval; + } + + void FixedUpdate() + { + if (!HighLogic.LoadedSceneIsFlight) return; + if (BDArmorySetup.GameIsPaused) return; + + if (enabling || disabling) + { + if (opticalReductionFactor < 1) + { + using (var Part = this.part.vessel.Parts.GetEnumerator()) + while (Part.MoveNext()) + { + if (Part.Current == null) continue; + Part.Current.SetOpacity(Mathf.Lerp(1, opticalReductionFactor, (cloakTimer / CloakTime))); + } + if (enabling) + { + cloakTimer += TimeWarp.fixedDeltaTime; + } + if (disabling) + { + cloakTimer -= TimeWarp.fixedDeltaTime; + } + } + //Debug.Log("[CloakingDevice] " + (enabling ? "cloaking" : "decloaking") + ": cloakTimer: " + cloakTimer); + } + if (cooldownTimer > 0) + { + cooldownTimer -= TimeWarp.fixedDeltaTime; + if (vessel.isActiveVessel) + { + gauge.UpdateHeatMeter(cooldownTimer / cooldownInterval); + } + } + } + + // RMB info in editor + public override string GetInfo() + { + StringBuilder output = new StringBuilder(); + output.AppendLine(OpticalCloaking ? ThermalCloaking ? "Thermoptic Cloak" : "Optical Cloak" : "ThermalCloak"); + if (OpticalCloaking) + { + output.AppendLine($" -View range reduction: {(1 - opticalReductionFactor) * 100}%"); + } + if (ThermalCloaking) + { + output.AppendLine($" - Heat signature reduction: {(1 - thermalReductionFactor * 100)}%"); + } + + output.AppendLine($"Always on: {alwaysOn}"); + output.AppendLine($"EC/sec: {resourceDrain}"); + + return output.ToString(); + } + } +} diff --git a/BDArmory/Modules/ModuleECMJammer.cs b/BDArmory/CounterMeasure/ModuleECMJammer.cs similarity index 79% rename from BDArmory/Modules/ModuleECMJammer.cs rename to BDArmory/CounterMeasure/ModuleECMJammer.cs index 9ce42e12e..297e37b2a 100644 --- a/BDArmory/Modules/ModuleECMJammer.cs +++ b/BDArmory/CounterMeasure/ModuleECMJammer.cs @@ -1,7 +1,7 @@ -using System.Text; -using BDArmory.CounterMeasure; +using BDArmory.UI; +using System.Text; -namespace BDArmory.Modules +namespace BDArmory.CounterMeasure { public class ModuleECMJammer : PartModule { @@ -13,6 +13,8 @@ public class ModuleECMJammer : PartModule [KSPField] public double resourceDrain = 5; + [KSPField] public string resourceName = "ElectricCharge"; + [KSPField] public bool alwaysOn = false; [KSPField] public bool signalSpam = true; @@ -21,11 +23,21 @@ public class ModuleECMJammer : PartModule [KSPField] public bool rcsReduction = false; + [KSPField] public float cooldownInterval = -1; + [KSPField(isPersistant = true, guiActive = true, guiName = "#LOC_BDArmory_Enabled")]//Enabled public bool jammerEnabled = false; + public bool manuallyEnabled = false; + + private int resourceID; + + private float cooldownTimer = 0; + VesselECMJInfo vesselJammer; + private BDStagingAreaGauge gauge; + [KSPAction("Enable")] public void AGEnable(KSPActionParam param) { @@ -62,13 +74,17 @@ public void Toggle() EnableJammer(); } } - + void Start() + { + resourceID = PartResourceLibrary.Instance.GetDefinition(resourceName).id; + } public override void OnStart(StartState state) { base.OnStart(state); if (!HighLogic.LoadedSceneIsFlight) return; part.force_activate(); + gauge = (BDStagingAreaGauge)part.AddModule("BDStagingAreaGauge"); GameEvents.onVesselCreate.Add(OnVesselCreate); } @@ -85,6 +101,7 @@ void OnVesselCreate(Vessel v) public void EnableJammer() { + if (cooldownTimer > 0) return; EnsureVesselJammer(); vesselJammer.AddJammer(this); jammerEnabled = true; @@ -96,11 +113,14 @@ public void DisableJammer() vesselJammer.RemoveJammer(this); jammerEnabled = false; + cooldownTimer = cooldownInterval; } public override void OnFixedUpdate() { base.OnFixedUpdate(); + if (!HighLogic.LoadedSceneIsFlight) return; + if (BDArmorySetup.GameIsPaused) return; if (alwaysOn && !jammerEnabled) { @@ -113,6 +133,14 @@ public override void OnFixedUpdate() DrainElectricity(); } + if (cooldownTimer > 0) + { + cooldownTimer -= TimeWarp.fixedDeltaTime; + if (vessel.isActiveVessel) + { + gauge.UpdateHeatMeter(cooldownTimer / cooldownInterval); + } + } } void EnsureVesselJammer() @@ -152,7 +180,7 @@ void DrainElectricity() } double drainAmount = resourceDrain * TimeWarp.fixedDeltaTime; - double chargeAvailable = part.RequestResource("ElectricCharge", drainAmount, ResourceFlowMode.ALL_VESSEL); + double chargeAvailable = part.RequestResource(resourceID, drainAmount, ResourceFlowMode.ALL_VESSEL); if (chargeAvailable < drainAmount * 0.95f) { DisableJammer(); diff --git a/BDArmory/CounterMeasure/VesselCloakInfo.cs b/BDArmory/CounterMeasure/VesselCloakInfo.cs new file mode 100644 index 000000000..c750d2e6e --- /dev/null +++ b/BDArmory/CounterMeasure/VesselCloakInfo.cs @@ -0,0 +1,158 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +using BDArmory.Modules; +using BDArmory.Utils; + +namespace BDArmory.CounterMeasure +{ + [RequireComponent(typeof(Vessel))] + public class VesselCloakInfo : MonoBehaviour + { + List cloaks; + public Vessel vessel; + + bool cEnabled; + + public bool cloakEnabled + { + get { return cEnabled; } + } + + float orf = 1; + public float opticalReductionFactor + { + get { return orf; } + } + + float trf = 1; + public float thermalReductionFactor + { + get { return trf; } + } + + void Awake() + { + cloaks = new List(); + vessel = GetComponent(); + + vessel.OnJustAboutToBeDestroyed += AboutToBeDestroyed; + GameEvents.onVesselCreate.Add(OnVesselCreate); + GameEvents.onPartJointBreak.Add(OnPartJointBreak); + GameEvents.onPartDie.Add(OnPartDie); + } + + void OnDestroy() + { + vessel.OnJustAboutToBeDestroyed -= AboutToBeDestroyed; + GameEvents.onVesselCreate.Remove(OnVesselCreate); + GameEvents.onPartJointBreak.Remove(OnPartJointBreak); + GameEvents.onPartDie.Remove(OnPartDie); + } + + void AboutToBeDestroyed() + { + Destroy(this); + } + + void OnPartDie(Part p = null) + { + if (gameObject.activeInHierarchy) + { + StartCoroutine(DelayedCleanCloakListRoutine()); + } + } + + void OnVesselCreate(Vessel v) + { + if (gameObject.activeInHierarchy) + { + StartCoroutine(DelayedCleanCloakListRoutine()); + } + } + + void OnPartJointBreak(PartJoint j, float breakForce) + { + if (gameObject.activeInHierarchy) + { + StartCoroutine(DelayedCleanCloakListRoutine()); + } + } + + public void AddCloak(ModuleCloakingDevice cloak) + { + if (!cloaks.Contains(cloak)) + { + cloaks.Add(cloak); + } + + UpdateCloakStrength(); + } + + public void RemoveCloak(ModuleCloakingDevice cloak) + { + cloaks.Remove(cloak); + + UpdateCloakStrength(); + } + + void UpdateCloakStrength() + { + cEnabled = cloaks.Count > 0; + + trf = 1; + orf = 1; + + using (List.Enumerator cloak = cloaks.GetEnumerator()) + while (cloak.MoveNext()) + { + if (cloak.Current == null) continue; + if (cloak.Current.thermalReductionFactor < trf) + { + trf = cloak.Current.thermalReductionFactor; + } + if (cloak.Current.opticalReductionFactor < orf) + { + orf = cloak.Current.opticalReductionFactor; + } + } + } + + public void DelayedCleanCloakList() + { + StartCoroutine(DelayedCleanCloakListRoutine()); + } + + IEnumerator DelayedCleanCloakListRoutine() + { + var wait = new WaitForFixedUpdate(); + yield return wait; + yield return wait; + CleanCloakList(); + } + + void CleanCloakList() + { + vessel = GetComponent(); + + if (!vessel) + { + Destroy(this); + } + cloaks.RemoveAll(j => j == null); + cloaks.RemoveAll(j => j.vessel != vessel); + + using (var cl = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) + while (cl.MoveNext()) + { + if (cl.Current == null) continue; + if (cl.Current.cloakEnabled) + { + AddCloak(cl.Current); + } + } + UpdateCloakStrength(); + } + } +} diff --git a/BDArmory/CounterMeasure/VesselECMJInfo.cs b/BDArmory/CounterMeasure/VesselECMJInfo.cs index 2991d7cce..179825929 100644 --- a/BDArmory/CounterMeasure/VesselECMJInfo.cs +++ b/BDArmory/CounterMeasure/VesselECMJInfo.cs @@ -1,8 +1,12 @@ using System.Collections; using System.Collections.Generic; -using BDArmory.Modules; using UnityEngine; +using BDArmory.Modules; +using BDArmory.Utils; +using BDArmory.Targeting; +using BDArmory.Radar; + namespace BDArmory.CounterMeasure { [RequireComponent(typeof(Vessel))] @@ -10,7 +14,7 @@ public class VesselECMJInfo : MonoBehaviour { List jammers; public Vessel vessel; - + private TargetInfo ti; bool jEnabled; public bool jammerEnabled @@ -38,12 +42,10 @@ public float rcsReductionFactor { get { return rcsr; } } - void Awake() { jammers = new List(); vessel = GetComponent(); - vessel.OnJustAboutToBeDestroyed += AboutToBeDestroyed; GameEvents.onVesselCreate.Add(OnVesselCreate); GameEvents.onPartJointBreak.Add(OnPartJointBreak); @@ -104,7 +106,7 @@ public void RemoveJammer(ModuleECMJammer jammer) UpdateJammerStrength(); } - void UpdateJammerStrength() + public void UpdateJammerStrength() { jEnabled = jammers.Count > 0; @@ -148,14 +150,50 @@ void UpdateJammerStrength() if (rcsrCount > 0) { - rcsr = Mathf.Clamp((rcsrTotal * rcsrCount), 0.0f, 1); //allow for 100% stealth (cloaking device) + rcsr = Mathf.Max((rcsrTotal * rcsrCount), 0.0f); //allow for 100% stealth (cloaking device) or stealth malus (radar reflectors) } else { rcsr = 1; } + + ti = RadarUtils.GetVesselRadarSignature(vessel); + ti.radarRCSReducedSignature = ti.radarBaseSignature; + ti.radarModifiedSignature = ti.radarBaseSignature; + ti.radarLockbreakFactor = 1; + //1) read vessel ecminfo for jammers with RCS reduction effect and multiply factor + ti.radarRCSReducedSignature *= rcsr; + ti.radarModifiedSignature *= rcsr; + //2) increase in detectability relative to jammerstrength and vessel rcs signature: + // rcs_factor = jammerStrength / modifiedSig / 100 + 1.0f + ti.radarModifiedSignature *= (((totaljStrength / ti.radarRCSReducedSignature) / 100) + 1.0f); + //3) garbling due to overly strong jamming signals relative to jammer's strength in relation to vessel rcs signature: + // jammingDistance = (jammerstrength / baseSig / 100 + 1.0) x js + ti.radarJammingDistance = ((totaljStrength / ti.radarBaseSignature / 100) + 1.0f) * totaljStrength; + //4) lockbreaking strength relative to jammer's lockbreak strength in relation to vessel rcs signature: + // lockbreak_factor = baseSig/modifiedSig x (1 � lopckBreakStrength/baseSig/100) + // Use clamp to prevent RCS reduction resulting in increased lockbreak factor, which negates value of RCS reduction) + ti.radarLockbreakFactor = (ti.radarRCSReducedSignature == 0) ? 0f : + Mathf.Max(Mathf.Clamp01(ti.radarRCSReducedSignature / ti.radarModifiedSignature) * (1 - (totalLBstrength / ti.radarRCSReducedSignature / 100)), 0); // 0 is minimum lockbreak factor + } + void OnFixedUpdate() + { + if (UI.BDArmorySetup.GameIsPaused) return; + if (jEnabled && jammerStrength > 0) + { + using (var loadedvessels = UI.BDATargetManager.LoadedVessels.GetEnumerator()) + while (loadedvessels.MoveNext()) + { + // ignore null, unloaded + if (loadedvessels.Current == null || !loadedvessels.Current.loaded || loadedvessels.Current == vessel) continue; + float distance = (loadedvessels.Current.CoM - vessel.CoM).magnitude; + if (distance < jammerStrength * 10) + { + RadarWarningReceiver.PingRWR(loadedvessels.Current, vessel.CoM, RadarWarningReceiver.RWRThreatTypes.Jamming, 0.2f); + } + } + } } - public void DelayedCleanJammerList() { StartCoroutine(DelayedCleanJammerListRoutine()); @@ -163,8 +201,9 @@ public void DelayedCleanJammerList() IEnumerator DelayedCleanJammerListRoutine() { - yield return null; - yield return null; + var wait = new WaitForFixedUpdate(); + yield return wait; + yield return wait; CleanJammerList(); } diff --git a/BDArmory/Damage/BuildingDamage.cs b/BDArmory/Damage/BuildingDamage.cs new file mode 100644 index 000000000..edef237af --- /dev/null +++ b/BDArmory/Damage/BuildingDamage.cs @@ -0,0 +1,67 @@ +using System.Linq; +using System.Collections.Generic; +using UnityEngine; +using BDArmory.Settings; + +namespace BDArmory.Damage +{ + [KSPAddon(KSPAddon.Startup.Flight, false)] + public class BuildingDamage : MonoBehaviour + { + static Dictionary buildingsDamaged = new Dictionary(); + + public static void RegisterDamage(DestructibleBuilding building) + { + if (!buildingsDamaged.ContainsKey(building)) + { + buildingsDamaged.Add(building, building.FacilityDamageFraction); + //Debug.Log("[BDArmory.BuildingDamage] registered " + building.name + " tracking " + buildingsDamaged.Count + " buildings"); + } + } + + void OnDestroy() + { + buildingsDamaged.Clear(); // Clear the damaged building tracker when leaving the flight scene to clear references to building objects. + } + + float buildingRegenTimer = 1; //regen 1 HP per second + float RegenFactor = 0.1f; //could always turn these into customizable settings if you want faster/slower healing buildings. 0.08f is enough for the browning to destroy some buildings but not others. + void FixedUpdate() + { + if (UI.BDArmorySetup.GameIsPaused) return; + + if (buildingsDamaged.Count > 0) + { + buildingRegenTimer -= Time.fixedDeltaTime; + if (buildingRegenTimer < 0) + { + foreach (var building in buildingsDamaged.Keys.ToList()) // Take a copy of the keys so we can modify the dictionary in the loop. + { + if (building == null) // Clear out any null references. + { + buildingsDamaged.Remove(building); + continue; + } + if (!building.IsIntact) + { + buildingsDamaged.Remove(building); + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log($"[BDArmory.BuildingDamage] building {building.name} destroyed! Removing"); + continue; + } + if (building.FacilityDamageFraction > buildingsDamaged[building]) + { + building.FacilityDamageFraction = Mathf.Max(building.FacilityDamageFraction - buildingsDamaged[building] * RegenFactor, buildingsDamaged[building]); // Heal up to the initial damage value. + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log($"[BDArmory.BuildingDamage] {building.name} current HP: {building.FacilityDamageFraction}"); + } + else + { + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log($"[BDArmory.BuildingDamage] {building.name} regenned to full HP, removing from list"); + buildingsDamaged.Remove(building); + } + } + buildingRegenTimer = 1; + } + } + } + } +} diff --git a/BDArmory.Core/Services/DamageService.cs b/BDArmory/Damage/DamageService.cs similarity index 75% rename from BDArmory.Core/Services/DamageService.cs rename to BDArmory/Damage/DamageService.cs index 50ce37b65..1575a8dea 100644 --- a/BDArmory.Core/Services/DamageService.cs +++ b/BDArmory/Damage/DamageService.cs @@ -1,7 +1,19 @@ -using BDArmory.Core.Events; +using System; -namespace BDArmory.Core.Services +using BDArmory.Services; + +namespace BDArmory.Damage { + [Serializable] + public class DamageEventArgs : EventArgs + { + public int VesselId { get; set; } + public int PartId { get; set; } + public float Damage { get; set; } + public float Armor { get; set; } + public DamageOperation Operation { get; set; } + } + public abstract class DamageService : NotificableService { public abstract void ReduceArmor_svc(Part p, float armorMass); diff --git a/BDArmory/Damage/HitpointTracker.cs b/BDArmory/Damage/HitpointTracker.cs new file mode 100644 index 000000000..b65401ce8 --- /dev/null +++ b/BDArmory/Damage/HitpointTracker.cs @@ -0,0 +1,1595 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using KSP.Localization; +using UnityEngine; + +using BDArmory.Armor; +using BDArmory.Extensions; +using BDArmory.Modules; +using BDArmory.Settings; +using BDArmory.Utils; + +namespace BDArmory.Damage +{ + public class HitpointTracker : PartModule, IPartMassModifier, IPartCostModifier + { + #region KSP Fields + public float GetModuleMass(float baseMass, ModifierStagingSituation situation) => armorMass + HullMassAdjust; + + public ModifierChangeWhen GetModuleMassChangeWhen() => ModifierChangeWhen.FIXED; + public float GetModuleCost(float baseCost, ModifierStagingSituation situation) => armorCost + HullCostAdjust; + public ModifierChangeWhen GetModuleCostChangeWhen() => ModifierChangeWhen.FIXED; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_Hitpoints"),//Hitpoints + UI_ProgressBar(affectSymCounterparts = UI_Scene.None, controlEnabled = false, scene = UI_Scene.All, maxValue = 100000, minValue = 0, requireFullControl = false)] + public float Hitpoints; + + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_ArmorThickness"),//Armor Thickness + UI_FloatRange(minValue = 0f, maxValue = 200, stepIncrement = 1f, scene = UI_Scene.All)] + public float Armor = 10f; //settable Armor thickness availible for editing in the SPH?VAB + + [KSPField(advancedTweakable = true, guiActive = true, guiActiveEditor = false, guiName = "#LOC_BDArmory_ArmorThickness")]//armor Thickness + public float Armour = 10f; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = false, guiName = "#LOC_BDArmory_ArmorRemaining"),//Armor intregity + UI_ProgressBar(affectSymCounterparts = UI_Scene.None, controlEnabled = false, scene = UI_Scene.Flight, maxValue = 100, minValue = 0, requireFullControl = false)] + public float ArmorRemaining = 100; + + public float StartingArmor; + + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_Armor_ArmorType"),//Armor Types + UI_FloatRange(minValue = 1, maxValue = 999, stepIncrement = 1, scene = UI_Scene.All)] + public float ArmorTypeNum = 1; //replace with prev/next buttons? //or a popup GUI box with a list of selectable types... + + //Add a part material type setting, so parts can be selected to be made out of wood/aluminium/steel to adjust base partmass/HP? + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_Armor_HullType"),//hull material Types + UI_FloatRange(minValue = 1, maxValue = 3, stepIncrement = 1, scene = UI_Scene.Editor)] + public float HullTypeNum = 2; + private float OldHullType = -1; + + [KSPField(isPersistant = true)] + public string hullType = "Aluminium"; + + [KSPField(guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_Armor_HullMat")]//Status + public string guiHullTypeString; + + public float HullMassAdjust = 0f; + public float HullCostAdjust = 0f; + double resourceCost = 0; + + private bool IgnoreForArmorSetup = false; + + private bool isAI = false; + + private bool isProcWing = false; + private bool isProcPart = false; + private bool waitingForHullSetup = false; + private float OldArmorType = -1; + + [KSPField(advancedTweakable = true, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_ArmorMass")]//armor mass + public float armorMass = 0f; + + private float totalArmorQty = 0f; + + [KSPField(advancedTweakable = true, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_ArmorCost")]//armor cost + public float armorCost = 0f; + + [KSPField(isPersistant = true)] + public string SelectedArmorType = "None"; //presumably Aubranium can use this to filter allowed/banned types + + [KSPField(guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_ArmorCurrent")]//Status + public string guiArmorTypeString = "def"; + + private ArmorInfo armorInfo; + private HullInfo hullInfo; + + private bool armorReset = false; + + [KSPField(isPersistant = true)] + public float maxHitPoints = -1f; + + [KSPField(isPersistant = true)] + public float ArmorThickness = 0f; + + [KSPField(isPersistant = true)] + public bool ArmorSet; + + [KSPField(isPersistant = true)] + public string ExplodeMode = "Never"; + + [KSPField(isPersistant = true)] + public bool FireFX = true; + + [KSPField(isPersistant = true)] + public float FireFXLifeTimeInSeconds = 5f; + + //Armor Vars + [KSPField(isPersistant = true)] + public float Density; + [KSPField(isPersistant = true)] + public float Diffusivity; + [KSPField(isPersistant = true)] + public float Ductility; + [KSPField(isPersistant = true)] + public float Hardness; + [KSPField(isPersistant = true)] + public float Strength; + [KSPField(isPersistant = true)] + public float SafeUseTemp; + [KSPField(isPersistant = true)] + public float Cost; + + [KSPField(isPersistant = true)] + public float vFactor; + [KSPField(isPersistant = true)] + public float muParam1; + [KSPField(isPersistant = true)] + public float muParam2; + [KSPField(isPersistant = true)] + public float muParam3; + [KSPField(isPersistant = true)] + public float muParam1S; + [KSPField(isPersistant = true)] + public float muParam2S; + [KSPField(isPersistant = true)] + public float muParam3S; + + [KSPField(isPersistant = true)] + public float HEEquiv; + [KSPField(isPersistant = true)] + public float HEATEquiv; + + [KSPField(isPersistant = true)] + public float maxForce; + [KSPField(isPersistant = true)] + public float maxTorque; + [KSPField(isPersistant = true)] + public double maxG; + + private bool startsArmored = false; + public bool ArmorPanel = false; + + //Part vars + private float partMass = 0f; + public Vector3 partSize; + [KSPField(isPersistant = true)] + public float maxSupportedArmor = -1; //upper cap on armor per part, overridable in MM/.cfg + [KSPField(isPersistant = true)] + public float armorVolume = -1; + private float sizeAdjust; + + AttachNode bottom; + AttachNode top; + + public List defaultShader; + public List defaultColor; + public bool RegisterProcWingShader = false; + + public float defenseMutator = 1; + + #endregion KSP Fields + + #region Heart Bleed + private double nextHeartBleedTime = 0; + #endregion Heart Bleed + + private readonly float hitpointMultiplier = BDArmorySettings.HITPOINT_MULTIPLIER; + + private float previousHitpoints = -1; + private bool previousEdgeLift = false; + private bool _updateHitpoints = false; + private bool _forceUpdateHitpointsUI = false; + private const int HpRounding = 25; + private bool _updateMass = false; + private bool _armorModified = false; + private bool _hullModified = false; + private bool _armorConfigured = false; + private bool _hullConfigured = false; + private bool _hpConfigured = false; + private bool _finished_setting_up = false; + public bool Ready => (_finished_setting_up || !HighLogic.LoadedSceneIsFlight) && _hpConfigured && _hullConfigured && _armorConfigured; + public string Why + { + get + { + if (Ready) return "Ready"; + else + { + List reasons = new List(); + if (!_finished_setting_up && HighLogic.LoadedSceneIsFlight) reasons.Add("still setting up"); + if (!_hpConfigured) reasons.Add("HP not configured"); + if (!_hullConfigured) reasons.Add("hull not configured"); + if (!_armorConfigured) reasons.Add("armor not configured"); + return string.Join(", ", reasons); + } + } + } + + public bool isOnFire = false; + + [KSPField(isPersistant = true)] + public float ignitionTemp = -1; + private double skinskinConduction = 1; + private double skinInternalConduction = 1; + + public override void OnLoad(ConfigNode node) + { + base.OnLoad(node); + + if (!HighLogic.LoadedSceneIsEditor && !HighLogic.LoadedSceneIsFlight) return; + + if (part.partInfo == null) + { + // Loading of the prefab from the part config + _updateHitpoints = true; + } + else + { + // Loading of the part from a saved craft + if (HighLogic.LoadedSceneIsEditor) + { + _updateHitpoints = true; + ArmorSet = false; + } + else // Loading of the part from a craft in flight mode + { + if (BDArmorySettings.RESET_HP && part.vessel != null) // Reset Max HP + { + var maxHPString = ConfigNodeUtils.FindPartModuleConfigNodeValue(part.partInfo.partConfig, "HitpointTracker", "maxHitPoints"); + if (!string.IsNullOrEmpty(maxHPString)) // Use the default value from the MM patch. + { + try + { + maxHitPoints = float.Parse(maxHPString); + if (BDArmorySettings.DEBUG_ARMOR) Debug.Log("[BDArmory.HitpointTracker]: setting maxHitPoints of " + part + " on " + part.vessel.vesselName + " to " + maxHitPoints); + _updateHitpoints = true; + } + catch (Exception e) + { + Debug.LogError("[BDArmory.HitpointTracker]: Failed to parse maxHitPoints configNode: " + e.Message); + } + } + else // Use the stock default value. + { + maxHitPoints = -1f; + } + } + else // Don't. + { + // enabled = false; // We'll disable this later once things are set up. + } + } + } + } + + public void SetupPrefab() + { + if (part != null) + { + ArmorRemaining = 100; + var maxHitPoints_ = CalculateTotalHitpoints(); + + if (!_forceUpdateHitpointsUI && previousHitpoints == maxHitPoints_) return; + + //Add Hitpoints + if (!ArmorPanel) + { + UI_ProgressBar damageFieldFlight = (UI_ProgressBar)Fields["Hitpoints"].uiControlFlight; + damageFieldFlight.maxValue = maxHitPoints_; + damageFieldFlight.minValue = 0f; + UI_ProgressBar damageFieldEditor = (UI_ProgressBar)Fields["Hitpoints"].uiControlEditor; + damageFieldEditor.maxValue = maxHitPoints_; + damageFieldEditor.minValue = 0f; + } + else + { + Fields["Hitpoints"].guiActive = false; + Fields["Hitpoints"].guiActiveEditor = false; + } + Hitpoints = maxHitPoints_; + if (!ArmorSet) overrideArmorSetFromConfig(); + + previousHitpoints = maxHitPoints_; + part.RefreshAssociatedWindows(); + } + else + { + if (BDArmorySettings.DEBUG_ARMOR) Debug.Log("[BDArmory.HitpointTracker]: OnStart part is null"); + } + } + + public override void OnStart(StartState state) + { + if (part == null) return; + isEnabled = true; + oldmaxHitpoints = maxHitPoints; + if (part.name.Contains("B9.Aero.Wing.Procedural")) + { + isProcWing = true; + } + if (part.name.Contains("procedural")) + { + isProcPart = true; + } + StartingArmor = Armor; + if (ProjectileUtils.IsArmorPart(this.part)) + { + ArmorPanel = true; + } + else + { + ArmorPanel = false; + } + if (!((HullTypeNum == 1 || HullTypeNum == 3) && hullType == "Aluminium")) //catch for legacy .craft files + { + HullTypeNum = HullInfo.materials.FindIndex(t => t.name == hullType) + 1; + } + if (SelectedArmorType == "Legacy Armor") + ArmorTypeNum = ArmorInfo.armors.FindIndex(t => t.name == "None"); + else + ArmorTypeNum = ArmorInfo.armors.FindIndex(t => t.name == SelectedArmorType) + 1; + guiArmorTypeString = SelectedArmorType; + guiHullTypeString = StringUtils.Localize(HullInfo.materials[HullInfo.materialNames[(int)HullTypeNum - 1]].localizedName); + + if (part.partInfo != null && part.partInfo.partPrefab != null) // PotatoRoid, I'm looking at you. + { + skinskinConduction = part.partInfo.partPrefab.skinSkinConductionMult; + skinInternalConduction = part.partInfo.partPrefab.skinSkinConductionMult; + } + if (HighLogic.LoadedSceneIsFlight) + { + if (BDArmorySettings.RESET_ARMOUR) + { + ArmorSetup(null, null); + } + if (BDArmorySettings.RESET_HULL || ArmorPanel) + { + IgnoreForArmorSetup = true; + HullTypeNum = HullInfo.materials.FindIndex(t => t.name == "Aluminium") + 1; + } + SetHullMass(); + part.RefreshAssociatedWindows(); + } + if (HighLogic.LoadedSceneIsFlight || HighLogic.LoadedSceneIsEditor) + { + int armorCount = 0; + for (int i = 0; i < ArmorInfo.armorNames.Count; i++) + { + armorCount++; + } + UI_FloatRange ATrangeEditor = (UI_FloatRange)Fields["ArmorTypeNum"].uiControlEditor; + ATrangeEditor.onFieldChanged = ArmorModified; + ATrangeEditor.maxValue = (float)armorCount; + int hullCount = 0; + for (int i = 0; i < HullInfo.materialNames.Count; i++) + { + hullCount++; + } + UI_FloatRange HTrangeEditor = (UI_FloatRange)Fields["HullTypeNum"].uiControlEditor; + HTrangeEditor.onFieldChanged = HullModified; + HTrangeEditor.maxValue = (float)hullCount; + if (ProjectileUtils.IsIgnoredPart(this.part)) + { + isAI = true; + Fields["ArmorTypeNum"].guiActiveEditor = false; + Fields["guiArmorTypeString"].guiActiveEditor = false; + Fields["guiArmorTypeString"].guiActive = false; + Fields["guiHullTypeString"].guiActiveEditor = false; + Fields["guiHullTypeString"].guiActive = false; + Fields["armorCost"].guiActiveEditor = false; + Fields["armorMass"].guiActiveEditor = false; + //UI_ProgressBar Armorleft = (UI_ProgressBar)Fields["ArmorRemaining"].uiControlFlight; + //Armorleft.scene = UI_Scene.None; + } + if (part.IsMissile()) + { + Fields["ArmorTypeNum"].guiActiveEditor = false; + Fields["guiArmorTypeString"].guiActiveEditor = false; + Fields["armorCost"].guiActiveEditor = false; + Fields["armorMass"].guiActiveEditor = false; + } + if (isAI || part.IsMissile()) + { + Fields["ArmorTypeNum"].guiActiveEditor = false; + ATrangeEditor.maxValue = 1; + } + if (BDArmorySettings.LEGACY_ARMOR || BDArmorySettings.RESET_ARMOUR) + { + Fields["ArmorTypeNum"].guiActiveEditor = false; + Fields["guiArmorTypeString"].guiActiveEditor = false; + Fields["guiArmorTypeString"].guiActive = false; + Fields["armorCost"].guiActiveEditor = false; + Fields["armorMass"].guiActiveEditor = false; + ATrangeEditor.maxValue = 1; + } + + //if part is an engine/fueltank don't allow wood construction/mass reduction + if (part.IsMissile() || part.IsWeapon() || ArmorPanel || isAI || BDArmorySettings.LEGACY_ARMOR || BDArmorySettings.RESET_HULL || ProjectileUtils.isMaterialBlackListpart(this.part)) + { + HullTypeNum = HullInfo.materials.FindIndex(t => t.name == "Aluminium") + 1; + HTrangeEditor.minValue = HullTypeNum; + HTrangeEditor.maxValue = HullTypeNum; + Fields["HullTypeNum"].guiActiveEditor = false; + Fields["HullTypeNum"].guiActive = false; + Fields["guiHullTypeString"].guiActiveEditor = false; + Fields["guiHullTypeString"].guiActive = false; + IgnoreForArmorSetup = true; + SetHullMass(); + } + if (ArmorThickness > 10 || ArmorPanel) //Set to 10, Cerulean's HP MM patches all have armorThickness 10 fields + { + startsArmored = true; + if (Armor > 10 && Armor != ArmorThickness) + { } + else + { + Armor = ArmorThickness; + } + //if (ArmorTypeNum == 1) + //{ + // ArmorTypeNum = 2; + //} + } + else + { + Fields["Armor"].guiActiveEditor = false; + Fields["guiArmorTypeString"].guiActiveEditor = false; + Fields["guiArmorTypeString"].guiActive = false; + Fields["armorCost"].guiActiveEditor = false; + Fields["armorMass"].guiActiveEditor = false; + } + } + GameEvents.onEditorShipModified.Add(ShipModified); + GameEvents.onPartDie.Add(OnPartDie); + bottom = part.FindAttachNode("bottom"); + top = part.FindAttachNode("top"); + //if (armorVolume < 0) //check already occurs 429, doubling it results in the PartSize vector3 returning null + calcPartSize(); + SetupPrefab(); + if (HighLogic.LoadedSceneIsEditor && !isProcWing) + { + var r = part.GetComponentsInChildren(); + { + for (int i = 0; i < r.Length; i++) + { + defaultShader.Add(r[i].material.shader); + if (BDArmorySettings.DEBUG_ARMOR) Debug.Log("[BDArmory.HitpointTracker]: ARMOR: part shader is " + r[i].material.shader.name); + if (r[i].material.HasProperty("_Color")) + { + defaultColor.Add(r[i].material.color); + } + } + } + } + Armour = Armor; + StartCoroutine(DelayedOnStart()); // Delay updating mass, armour, hull and HP so mods like proc wings and tweakscale get the right values. + //if (HighLogic.LoadedSceneIsFlight) + //{ + //if (BDArmorySettings.DEBUG_ARMOR) + //Debug.Log("[BDArmory.HitpointTracker]: ARMOR: part mass is: " + (part.mass - armorMass) + "; Armor mass is: " + armorMass + "; hull mass adjust: " + HullMassAdjust + "; total: " + part.mass); + //} + CalculateDryCost(); + } + + void calcPartSize() + { + partSize = Vector3.zero; + int topSize = 0; + int bottomSize = 0; + try + { + if (top != null) + { + topSize = top.size; + } + if (bottom != null) + { + bottomSize = bottom.size; + } + } + catch + { + Debug.Log("[BDArmoryHitpointTracker]: no node size detected"); + } + //if attachnode top != bottom, then cone. is nodesize Attachnode.radius or Attachnode.size? + //getSize returns size of a rectangular prism; most parts are circular, some are conical; use sizeAdjust to compensate + if (bottom != null && top != null) //cylinder + { + sizeAdjust = 0.783f; + } + else if ((bottom == null && top != null) || (bottom != null && top == null) || (topSize > bottomSize || bottomSize > topSize)) //cone + { + sizeAdjust = 0.422f; + } + else //no bottom or top nodes, assume srf attached part; these are usually panels of some sort. Will need to determine method of ID'ing triangular panels/wings + { //Wings at least could use WingLiftArea as a workaround for approx. surface area... + sizeAdjust = 0.5f; //armor on one side, otherwise will have armor thickness on both sides of the panel, nonsensical + double weight + } + partSize = CalcPartBounds(this.part, this.transform).size; + if (armorVolume < 0 || HighLogic.LoadedSceneIsEditor && isProcPart) //make this persistant to get around diffeences in part bounds between SPH/Flight. Also reset if in editor and a procpart to account for resizing + { + armorVolume = // thickness * armor mass; moving it to Start since it only needs to be calc'd once + ((((partSize.x * partSize.y) * 2) + ((partSize.x * partSize.z) * 2) + ((partSize.y * partSize.z) * 2)) * sizeAdjust); //mass * surface area approximation of a cylinder, where H/W are unknown + if (HighLogic.LoadedSceneIsFlight) //Value correction for loading legacy craft via VesselMover spawner/tournament autospawn that haven't got a armorvolume value in their .craft file. + { + armorVolume *= 0.63f; //part bounds dimensions when calced in Flight are consistantly 1.6-1.7x larger than correct SPH dimensions. Won't be exact, but good enough for legacy craft support + } + if (BDArmorySettings.DEBUG_ARMOR) Debug.Log("[BDArmory.HitpointTracker]: ARMOR: part size is (X: " + partSize.x + ";, Y: " + partSize.y + "; Z: " + partSize.z); + if (BDArmorySettings.DEBUG_ARMOR) Debug.Log("[BDArmory.HitpointTracker]: ARMOR: size adjust mult: " + sizeAdjust + "; part srf area: " + armorVolume); + } + } + + IEnumerator DelayedOnStart() + { + yield return new WaitForFixedUpdate(); + if (part == null) yield break; + if (part.GetComponent()) + { + var tic = Time.time; + yield return new WaitUntilFixed(() => part == null || part.mass > 0 || Time.time - tic > 5); // Give it 5s to get the part info. + if (part != null) + { + partMass = part.mass; + calcPartSize(); // Re-calculate the size. + SetupPrefab(); // Re-setup the prefab. + } + } + if (part.partInfo != null && part.partInfo.partPrefab != null) partMass = part.partInfo.partPrefab.mass; + _updateMass = true; + _armorModified = true; + _hullModified = true; + _updateHitpoints = true; + } + + private void OnDestroy() + { + if (bottom != null) bottom = null; + if (top != null) top = null; + GameEvents.onEditorShipModified.Remove(ShipModified); + GameEvents.onPartDie.Remove(OnPartDie); + } + + void OnPartDie() { OnPartDie(part); } + + void OnPartDie(Part p) + { + if (p == part) + { + Destroy(this); // Force this module to be removed from the gameObject as something is holding onto part references and causing a memory leak. + } + } + + public void ShipModified(ShipConstruct data) + { + // Note: this triggers if the ship is modified, but really we only want to run this when the part is modified. + if (isProcWing || isProcPart) + { + if (!_delayedShipModifiedRunning) + { + StartCoroutine(DelayedShipModified()); + if (!part.name.Contains("B9.Aero.Wing.Procedural.Panel") && !previousEdgeLift) ProceduralWing.ResetPWing(part); + previousEdgeLift = true; + } + + } + else + { + _updateHitpoints = true; + _updateMass = true; + } + } + + private bool _delayedShipModifiedRunning = false; + IEnumerator DelayedShipModified() // Wait a frame before triggering to allow proc wings to update it's mass properly. + { + _delayedShipModifiedRunning = true; + yield return new WaitForFixedUpdate(); + _delayedShipModifiedRunning = false; + if (part == null) yield break; + _updateHitpoints = true; + _updateMass = true; + } + + public void ArmorModified(BaseField field, object obj) + { + _armorModified = true; + foreach (var p in part.symmetryCounterparts) + { + var hp = p.GetComponent(); + if (hp == null) continue; + hp._armorModified = true; + } + } + public void HullModified(BaseField field, object obj) + { + _hullModified = true; + foreach (var p in part.symmetryCounterparts) + { + var hp = p.GetComponent(); + if (hp == null) continue; + hp._hullModified = true; + } + } + + void Update() + { + if (_finished_setting_up) // Only gets set in flight mode. + { + RefreshHitPoints(); + return; + } + if (HighLogic.LoadedSceneIsEditor || HighLogic.LoadedSceneIsFlight) // Also needed in flight mode for initial setup of mass, hull and HP, but shouldn't be triggered afterwards as ShipModified is only for the editor. + { + if (_armorModified) + { + _armorModified = false; + ArmorSetup(null, null); + } + if (_hullModified && !_updateMass) // Wait for the mass to update first. + { + _hullModified = false; + HullSetup(null, null); + } + if (!_updateMass) // Wait for the mass to update first. + RefreshHitPoints(); + if (HighLogic.LoadedSceneIsFlight && _armorConfigured && _hullConfigured && _hpConfigured) // No more changes, we're done. + { + _finished_setting_up = true; + } + } + } + + void FixedUpdate() + { + if (_updateMass) + { + _updateMass = false; + var oldPartMass = partMass; + var oldHullMassAdjust = HullMassAdjust; // We need to temporarily remove the HullmassAdjust and update the part.mass to get the correct value as KSP clamps the mass to > 1e-4. + HullMassAdjust = 0; + part.UpdateMass(); + //partMass = part.mass - armorMass - HullMassAdjust; //part mass is taken from the part.cfg val, not current part mass; this overrides that + //need to get ModuleSelfSealingTank mass adjustment. Could move the SST module to BDA.Core + if (isProcWing || isProcPart) + { + float Safetymass = 0; + var SST = part.GetComponent(); + if (SST != null) + { Safetymass = SST.FBmass + SST.FISmass; } + partMass = part.mass - armorMass - HullMassAdjust - Safetymass; + } + CalculateDryCost(); //recalc if modify event added a fueltank -resource swap, etc + HullMassAdjust = oldHullMassAdjust; // Put the HullmassAdjust back so we can test against it when we update the hull mass. + if (oldPartMass != partMass) + { + if (BDArmorySettings.DEBUG_ARMOR) Debug.Log($"[BDArmory.HitpointTracker]: {part.name} updated mass at {Time.time}: part.mass {part.mass}, partMass {oldPartMass}->{partMass}, armorMass {armorMass}, hullMassAdjust {HullMassAdjust}"); + if (isProcPart) + { + calcPartSize(); + _armorModified = true; + } + _hullModified = true; // Modifying the mass modifies the hull. + _updateHitpoints = true; + } + } + + if (HighLogic.LoadedSceneIsFlight && !UI.BDArmorySetup.GameIsPaused) + { + if (BDArmorySettings.HEART_BLEED_ENABLED && ShouldHeartBleed()) + { + HeartBleed(); + } + //if (ArmorTypeNum > 1 || ArmorPanel) + if (ArmorTypeNum != (ArmorInfo.armors.FindIndex(t => t.name == "None") + 1) || ArmorPanel) + { + if (part.skinTemperature > SafeUseTemp * 1.5f) + { + ReduceArmor((armorVolume * ((float)part.skinTemperature / SafeUseTemp)) * TimeWarp.fixedDeltaTime); //armor's melting off ship + } + } + if (!BDArmorySettings.BD_FIRES_ENABLED || !BDArmorySettings.BD_FIRE_HEATDMG) return; // Disabled. + + if (BDArmorySettings.BD_FIRES_ENABLED && BDArmorySettings.BD_FIRE_HEATDMG) + { + if (!isOnFire) + { + if (ignitionTemp > 0 && part.temperature > ignitionTemp) + { + string fireStarter; + var vesselFire = part.vessel.GetComponentInChildren(); + if (vesselFire != null) + { + fireStarter = vesselFire.SourceVessel; + } + else + { + fireStarter = part.vessel.GetName(); + } + FX.BulletHitFX.AttachFire(transform.position, part, 50, fireStarter); + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log($"[BDarmory.HitPointTracker]: Hull auto-ignition! {part.name} is on fire!; temperature: {part.temperature}"); + isOnFire = true; + } + } + } + } + } + private void RefreshHitPoints() + { + if (_updateHitpoints) + { + _updateHitpoints = false; + _forceUpdateHitpointsUI = false; + SetupPrefab(); + } + } + + #region HeartBleed + private bool ShouldHeartBleed() + { + // wait until "now" exceeds the "next tick" value + double dTime = Planetarium.GetUniversalTime(); + if (dTime < nextHeartBleedTime) + { + //Debug.Log(string.Format("[BDArmory.HitpointTracker]: TimeSkip ShouldHeartBleed for {0} on {1}", part.name, part.vessel.vesselName)); + return false; + } + + // assign next tick time + double interval = BDArmorySettings.HEART_BLEED_INTERVAL; + nextHeartBleedTime = dTime + interval; + + return true; + } + + private void HeartBleed() + { + float rate = BDArmorySettings.HEART_BLEED_RATE; + float deduction = Hitpoints * rate; + if (Hitpoints - deduction < BDArmorySettings.HEART_BLEED_THRESHOLD) + { + // can't die from heart bleed + return; + } + // deduct hp base on the rate + //Debug.Log(string.Format("[BDArmory.HitpointTracker]: Heart bleed {0} on {1} by {2:#.##} ({3:#.##}%)", part.name, part.vessel.vesselName, deduction, rate*100.0)); + AddDamage(deduction); + } + #endregion + + #region Hitpoints Functions + + //[KSPField(isPersistant = true)] + //public bool HPMode = false; + float oldmaxHitpoints; + /* + [KSPEvent(advancedTweakable = true, guiActive = false, guiActiveEditor = true, guiName = "Toggle HP Calc", active = true)]//Self-Sealing Tank + public void ToggleHPOption() + { + HPMode = !HPMode; + if (!HPMode) + { + Events["ToggleHPOption"].guiName = StringUtils.Localize("Revert to Legacy HP calc"); + maxHitPoints = oldmaxHitpoints; + } + else + { + Events["ToggleHPOption"].guiName = StringUtils.Localize("Test Refactored Calc"); + oldmaxHitpoints = maxHitPoints; + maxHitPoints = -1; + } + SetupPrefab(); + GUIUtils.RefreshAssociatedWindows(part); + } + */ + public float CalculateTotalHitpoints() + { + float hitpoints;// = -1; + + if (!part.IsMissile()) + { + if (!ArmorPanel) + { + if (maxHitPoints <= 0) + { + bool clampHP = false; + float structuralMass = 100; + float structuralVolume = 1; + float density = 1; + //if (!HPMode) + { + var averageSize = part.GetAverageBoundSize(); + var sphereRadius = averageSize * 0.5f; + var sphereSurface = 4 * Mathf.PI * sphereRadius * sphereRadius; + var thickness = 0.1f;// * part.GetTweakScaleMultiplier(); // Tweakscale scales mass as r^3 insted of 0.1*r^2, however it doesn't take the increased volume of the hull into account when scaling resource amounts. + structuralVolume = Mathf.Max(sphereSurface * thickness, 1e-3f); // Prevent 0 volume, just in case. structural volume is 10cm * surface area of equivalent sphere. + //bool clampHP = false; + + density = (partMass * 1000f) / structuralVolume; + if (density > 1e5f || density < 10) + { + if (BDArmorySettings.DEBUG_ARMOR) Debug.Log($"[BDArmory.HitpointTracker]: {part.name} extreme density detected: {density}! Trying alternate approach based on partSize."); + //structuralVolume = (partSize.x * partSize.y + partSize.x * partSize.z + partSize.y * partSize.z) * 2f * sizeAdjust * Mathf.PI / 6f * 0.1f; // Box area * sphere/cube ratio * 10cm. We use sphere/cube ratio to get similar results as part.GetAverageBoundSize(). + structuralVolume = armorVolume * Mathf.PI / 6f * 0.1f; //part bounds change between editor and flight, so use existing persistant size value + density = (partMass * 1000f) / structuralVolume; + if (density > 1e5f || density < 10) + { + if (BDArmorySettings.DEBUG_ARMOR) Debug.Log($"[BDArmory.HitpointTracker]: {part.name} still has extreme density: {density}! Setting HP based only on mass instead."); + clampHP = true; + } + } + density = Mathf.Clamp(density, 1000, 10000); + //if (BDArmorySettings.DEBUG_LABELS) + //Debug.Log("[BDArmory.HitpointTracker]: Hitpoint Calc" + part.name + " | structuralVolume : " + structuralVolume); + // if (BDArmorySettings.DEBUG_LABELS) Debug.Log("[BDArmory.HitpointTracker]: Hitpoint Calc" + part.name + " | Density : " + density); + + structuralMass = density * structuralVolume; //this just means hp = mass if the density is within the limits. + + //bigger things need more hp; but things that are denser, should also have more hp, so it's a bit more complicated than have hp = volume * hp mult + //hp = (volume * Hp mult) * density mod? + //lets take some examples; 3 identical size parts, mk1 cockpit(930kg), mk1 stuct tube (100kg), mk1 LF tank (250kg) + //if, say, a Hp mod of 300, so 2.55m3 * 300 = 765 -> 800hp + //cockpit has a density of ~364, fueltank of 98, struct tube of 39 + //density can't be linear scalar. Cuberoot? would need to reduce hp mult. + //2.55 * 100* 364^1/3 = 1785, 2.55 * 100 * 98^1/3 = 1157, 2.55 * 100 * 39^1/3 = 854 + + // if (BDArmorySettings.DEBUG_LABELS) Debug.Log("[BDArmory.HitpointTracker]: " + part.name + " structural Volume: " + structuralVolume + "; density: " + density); + //3. final calculations + hitpoints = structuralMass * hitpointMultiplier * 0.333f; + + } + /* + else //revised HP calc, commented out for now until we get feedback on new method and decide to switch over + { + //var averageSize = part.GetVolume(); // this grabs x/y/z dimensions from PartExtensions.cs + var averageSize = partSize.x * partSize.y * partSize.z; + structuralVolume = averageSize * sizeAdjust; //a cylinder diameter X length y is ~78.5% the volume of a rectangle of h/w x, length y. + //(mk2 parts are ~66% volume of equivilent rectangle, but are reinforced hulls, so.. + //cones are ~36-37% volume + //parts that aren't cylinders or close enough and need exceptions: Wings, control surfaces, radiators/solar panels + //var dryPartmass = part.mass - part.resourceMass; + var dryPartmass = part.mass; + density = (dryPartmass * 1000) / structuralVolume; + //var structuralMass = density * structuralVolume; // this means HP is solely determined my part mass, after assuming all parts have min density of 1000kg/m3 + //Debug.Log("[BDArmory]: Hitpoint Calc" + part.name + " | structuralVolume : " + structuralVolume); + + if (!part.IsAero() && !isProcPart && !isProcWing) + { + if (part.IsMotor()) + { + //hitpoints = ((dryPartmass * density) * 4) * hitpointMultiplier * 0.33f; // engines in KSP are very dense - leads to massive HP due to large mass, small volume. Engines also don't respond well to being shot, so... + //juno vol: 0.105, density: 2370; Ideal HP: ~300? + //wheesley: 0.843, 1777; ~1000 + //panther: 1.181, 1015; ~800 //low-bypass turbofans are going to be denser, have more of their volume susseptable to damage + //goliath: 16.38?, 274 ~2000? //massive turbofans would be less vulnerable to lead injestion, depending on how hardened the engine is against birdstrikes/FOD; they're also something like 50% open space + //hitpoints = structuralVolume * 100 * Mathf.Pow(density, 1/3) * hitpointMultiplier * 0.33f; + //gives 150 for the juno, 1025 for the wheesley, 1200 for the panther, 10625/goliath + //(drymass + volume) * (density / 2)? + //Juno - 420; wheesley: 2100; panther: 1225; goliath: 2875 + //volume * density + //...that's just HP = partmass + //that said, that could work... Juno: 250HP; wheesley: 1500HP; panther; 1200HP; goliath: 4500 HP; M3X Wyvern: 8000 HP; those numbers *do* look reasonable for engines... + //whiplash/rapier would be 1.8/2k HP, which is pushing it a bit... look into a clamp of some sort + //Rapier vol/density is ~0.92, 2171. clamp density to partmass? 2000? + //volume * mathf.clamp(density, 100, 1750) ? + hitpoints = structuralVolume * Mathf.Clamp(density, 100, 1750) * hitpointMultiplier * 0.33f; + if (hitpoints > (dryPartmass * 2000) || hitpoints < (dryPartmass * 750)) + { + hitpoints = Mathf.Clamp(hitpoints, (dryPartmass * 750), (dryPartmass * 2000)); // if HP is 10x more or 10x than 1/10th drymass in kg, clamp to 10x more/less + } + } + else + { + if (dryPartmass < 1) + { + density = Mathf.Clamp(density, 60, 150);// things like crew cabins are heavy, but most of that mass isn't going to be structural plating, so lets limit structural density + // important to note: a lot of the HP values in the old system came from the calculation assuming everytihng had a minimum density of 1000kg/m3 + //hitpoints = ((dryPartmass * density) * 20) * hitpointMultiplier * 0.33f; //multiplying mass by density extrapolates volume, so parts with the same vol, but different mass appropriately affected (eg Mk1 strucural fuselage vs mk1 LF tank + //as well as parts of different vol, but same density - all fueltanks - similarly affected + //2.55 * 100* 364^1/3 = 1785, 2.55 * 100 * 98^1/3 = 1157, 2.55 * 100 * 39^1/3 = 854 + hitpoints = structuralVolume * 60 * Mathf.Pow(density, 0.333f) * hitpointMultiplier * 0.33f; + if (hitpoints > (dryPartmass * 3500) || hitpoints < (dryPartmass * 350)) + { + //Debug.Log($"[BDArmory]: HitpointTracker::Clamping hitpoints for part {part.name}"); + hitpoints = Mathf.Clamp(hitpoints, (dryPartmass * 350), (dryPartmass * 3500)); // if HP is 10x more or 10x than 1/10th drymass in kg, clamp to 10x more/less + } + } + else + { + density = Mathf.Clamp(density, 40, 120); //lower stuctural density on very large parts to prevent HP bloat + hitpoints = structuralVolume * 40 * Mathf.Pow(density, 0.333f) * hitpointMultiplier * 0.33f; + //logarithmic scaling past a threshold (2k...?) investigate how this affects S2/3/4 tanks/Mk3 parts, etc + if (hitpoints > (dryPartmass * 2500) || hitpoints < (dryPartmass * 250)) + { + //Debug.Log($"[BDArmory]: HitpointTracker::Clamping hitpoints for part {part.name}"); + hitpoints = Mathf.Clamp(hitpoints, (dryPartmass * 250), (dryPartmass * 2500)); // if HP is 10x more or 10x than 1/10th drymass in kg, clamp to 10x more/less + } + } + } + } + if (part.IsAero() && !isProcWing) + { + //hitpoints = dryPartmass * 7000 * hitpointMultiplier * 0.333f; //stock wing parts are 700 HP per unit of Lift, 10 lift/1000kg + hitpoints = (float)part.Modules.GetModule().deflectionLiftCoeff * 700 * hitpointMultiplier * 0.333f; //stock wings are 700 HP per lifting surface area; using lift instead of mass (110 Lift/ton) due to control surfaces weighing more + } + } + */ + if (isProcPart) + { + structuralVolume = armorVolume * Mathf.PI / 6f * 0.1f; // Box area * sphere/cube ratio * 10cm. We use sphere/cube ratio to get similar results as part.GetAverageBoundSize(). + density = (partMass * 1000f) / structuralVolume; + //if (density > 1e5f || density < 10) + if (density > 1e5f || density < 145) //this should cause HP clamping for hollow parts when they reach stock Struct tube thickness or therabouts + { + if (BDArmorySettings.DEBUG_ARMOR) Debug.Log($"[BDArmory.HitpointTracker]: procPart {part.name} still has extreme density: {density}! Setting HP based only on mass instead."); + clampHP = true; + } + //density = Mathf.Clamp(density, 500, 10000); + density = Mathf.Clamp(density, 250, 10000); + structuralMass = density * structuralVolume; + //might instead need to grab Procpart mass/size vars via reflection + hitpoints = (structuralMass * hitpointMultiplier * 0.333f) * 5.2f; + } + if (clampHP) + { + if (BDArmorySettings.DEBUG_ARMOR) Debug.Log($"[BDArmory.HitpointTracker]: Clamping hitpoints for Procpart {part.name} from {hitpoints} to {hitpointMultiplier * (partMass * 100) * 333f}"); + //hitpoints = hitpointMultiplier * partMass * 333f; + hitpoints = hitpointMultiplier * (partMass * 10) * 250; //to not have Hp immediately get clamped to 25 + } + //hitpoints = (structuralVolume * Mathf.Pow(density, .333f) * Mathf.Clamp(80 - (structuralVolume / 2), 80 / 4, 80)) * hitpointMultiplier * 0.333f; //volume * cuberoot of density * HP mult scaled by size + + if (isProcWing) + { + if (!BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.PWING_EDGE_LIFT) + { + if (FerramAerospace.CheckForFAR()) //half-baked legacy method that we're stuck with lest FJRT whine + { + //procwing hp already modified by mass, because it is mass + //so using base part mass as it can be properly modified by material HP mod below + if (BDArmorySettings.DEBUG_ARMOR) Debug.Log($"[BDArmory.HitpointTracker]: Found {part.name} (FAR); HP: {Hitpoints}->{hitpoints} at time {Time.time}, partMass: {partMass}, FAR massMult: {FerramAerospace.GetFARMassMult(part)}"); + //hitpoints = ((partMass / FerramAerospace.GetFARMassMult(part)) * 1000f) * 3.5f * hitpointMultiplier * 0.333f; //To account for FAR's Strength-mass Scalar. + hitpoints = (partMass * 1000f) * 3.5f * hitpointMultiplier * 0.333f; + armorVolume = (float)Math.Round(hitpoints / hitpointMultiplier / 0.333 / 175, 1) / FerramAerospace.GetFARMassMult(part); //half of HP due to wing's 0.5x area modifier to prevent double armor + } + else + { + hitpoints = (float)Math.Round(part.Modules.GetModule() ? part.Modules.GetModule().deflectionLiftCoeff : partMass * 10, 2) * 700 * hitpointMultiplier * 0.333f; //use mass*10 for wings (since they may have lift toggled off), use lift area for control surfaces + armorVolume = (float)Math.Round(hitpoints / hitpointMultiplier / 0.333 / 350, 1); //stock is 0.25 lift/m2, so... //edges contribute to HP when they shouldn't; suggestion was to use tank volume instead (which would also allow thickness to play a role in HP), try ProceduralWing.aeroStatVolume * 700 + } + } + if (!BDArmorySettings.PWING_EDGE_LIFT || BDArmorySettings.PWING_THICKNESS_AFFECT_MASS_HP || BDArmorySettings.RUNWAY_PROJECT || part.name.Contains("B9.Aero.Wing.Procedural.Panel")) //method to make pwings balanced with stock. + { + hitpoints = -1; + armorVolume = -1; + if (ProceduralWing.CheckForB9ProcWing() && ProceduralWing.CheckForPWModule()) + { + float aeroVolume = ProceduralWing.GetPWingVolume(part); //PWing 0.7 * length * (widthRoot + WidthTip) + (thicknessRoot + ThicknessTip) / 4; yields 1.008 for a stock dimension 2*4*.18 board, so need mult of 1400 for parity with stock wing boards + if (BDArmorySettings.DEBUG_ARMOR) Debug.Log($"[BDArmory.HitpointTracker]: Found {part.name}; HP: {Hitpoints}->{hitpoints} at time {Time.time}, partMass: {partMass}, Pwing Aerovolume: {aeroVolume}"); + hitpoints = (float)Math.Round(part.Modules.GetModule() ? part.Modules.GetModule().deflectionLiftCoeff * 700 : (aeroVolume * 1400), 2) * hitpointMultiplier * 0.333f; //use volume for wings (since they may have lift toggled off), use lift area for control surfaces + //hitpoints should scale with stock wings correctly (and if used as thicker structural elements, should scale with tanks of similar size) + if (FerramAerospace.CheckForFAR()) + { + if (BDArmorySettings.DEBUG_ARMOR) Debug.Log($"[BDArmory.HitpointTracker]: Found {part.name} (FAR); HP: {Hitpoints}->{hitpoints} at time {Time.time}, partMass: {partMass}, FAR massMult: {FerramAerospace.GetFARMassMult(part)}"); + hitpoints *= FerramAerospace.GetFARMassMult(part); //PWing HP no longer mass dependant, so lets have FAR's structural strengthening/weakening have an effect on HP. you want light wings? they're going to be fragile, and vice versa + } + armorVolume = ProceduralWing.GetPWingArea(part); + if (!part.name.Contains("B9.Aero.Wing.Procedural.Panel")) + { + previousEdgeLift = false; + } + else + { + if (HighLogic.LoadedSceneIsFlight) + { + var lift = part.FindModuleImplementing(); + if (lift != null) lift.deflectionLiftCoeff = 0; + DragCube DragCube = DragCubeSystem.Instance.RenderProceduralDragCube(part); + part.DragCubes.ClearCubes(); + part.DragCubes.Cubes.Add(DragCube); + part.DragCubes.ResetCubeWeights(); + part.DragCubes.ForceUpdate(true, true, false); + part.DragCubes.SetDragWeights(); + if (HighLogic.LoadedSceneIsEditor) GameEvents.onEditorShipModified.Fire(EditorLogic.fetch.ship); + } + } + if (BDArmorySettings.RUNWAY_PROJECT_ROUND == 60) hitpoints = Mathf.Min(500, hitpoints); + } + } + if (hitpoints < 0) //sanity checks + { + if (BDArmorySettings.DEBUG_ARMOR) Debug.Log($"[BDArmory.HitpointTracker]: Aerovolume not found, reverting to lift/mass HP Calc!"); + hitpoints = (float)Math.Round(part.Modules.GetModule() ? part.Modules.GetModule().deflectionLiftCoeff : partMass * 10, 2) * 700 * hitpointMultiplier * 0.333f; //use mass*10 for wings (since they may have lift toggled off), use lift area for control surfaces + } + if (armorVolume < 0) + { + if (BDArmorySettings.DEBUG_ARMOR) Debug.Log($"[BDArmory.HitpointTracker]: AeroArea not found, reverting to Hitpoint Armorvolume calc!"); + armorVolume = (float)Math.Round(hitpoints / hitpointMultiplier / 0.333 / 350, 1); //stock is 0.25 lift/m2, so... + } + ArmorModified(null, null); + } + if ((BDArmorySettings.RUNWAY_PROJECT || BDArmorySettings.HP_THRESHOLD >= 100) && hitpoints > BDArmorySettings.HP_THRESHOLD) //If RunwayProject or Clamped HP setting, clamp HP + { + var scale = (BDArmorySettings.HP_THRESHOLD >= 100 ? BDArmorySettings.HP_THRESHOLD : 2000f) / (Mathf.Exp(1) - 1); + hitpoints = Mathf.Min(hitpoints, (BDArmorySettings.HP_THRESHOLD >= 100 ? BDArmorySettings.HP_THRESHOLD : 2000f) * Mathf.Log(hitpoints / scale + 1)); //use default of 2K for RP if slider set to unclamped + } + hitpoints = BDAMath.RoundToUnit(hitpoints, HpRounding); + //hitpoints = Mathf.Round(hitpoints);//? + if (hitpoints < 100) hitpoints = 100; + hitpoints *= HullInfo.materials[hullType].healthMod; // Apply health mod after rounding and lower limit. + if (BDArmorySettings.DEBUG_ARMOR && maxHitPoints <= 0 && Hitpoints != hitpoints) Debug.Log($"[BDArmory.HitpointTracker]: {part.name} updated HP: {Hitpoints}->{hitpoints} at time {Time.time}, partMass: {partMass}, density: {density}, structuralVolume: {structuralVolume}, structuralMass {structuralMass}"); + } + else // Override based on part configuration for custom parts + { + hitpoints = maxHitPoints * HullInfo.materials[hullType].healthMod; + //hitpoints = Mathf.Round(hitpoints); // / HpRounding) * HpRounding; + + if (BDArmorySettings.DEBUG_ARMOR && maxHitPoints <= 0 && Hitpoints != hitpoints) Debug.Log($"[BDArmory.HitpointTracker]: {part.name} updated HP: {Hitpoints}->{hitpoints} at time {Time.time}"); + } + } + else + { + hitpoints = ArmorRemaining; // * armorVolume * 10; + //hitpoints = Mathf.Round(hitpoints / HpRounding) * HpRounding; + //armorpanel HP is panel integrity, as 'HP' is the slab of armor; having a secondary unused HP pool will only make armor massively more effective against explosions than it should due to how isInLineOfSight calculates intermediate parts + } + } + else + { + hitpoints = maxHitPoints > 0 ? maxHitPoints : 5; + Armor = ArmorThickness > 0 ? ArmorThickness : 2; + } + if (!_finished_setting_up && _armorConfigured && _hullConfigured) _hpConfigured = true; + if (BDArmorySettings.HP_CLAMP >= 100) + hitpoints = Mathf.Min(hitpoints, BDArmorySettings.HP_CLAMP); + return hitpoints; + } + + public void DestroyPart() + { + if ((part.mass - armorMass) <= 2f) part.explosionPotential *= 0.85f; + + PartExploderSystem.AddPartToExplode(part); + } + + public float GetMaxArmor() + { + UI_FloatRange armorField = (UI_FloatRange)Fields["Armor"].uiControlEditor; + return armorField.maxValue; + } + + public float GetMaxHitpoints() + { + UI_ProgressBar hitpointField = (UI_ProgressBar)Fields["Hitpoints"].uiControlEditor; + return hitpointField.maxValue; + } + + public bool GetFireFX() + { + return FireFX; + } + + public void SetDamage(float partdamage) + { + Hitpoints = partdamage; //given the sole reference is from destroy, with damage = -1, shouldn't this be =, not -=? + + if (Hitpoints <= 0) + { + if (BDArmorySettings.DEBUG_ARMOR) Debug.Log($"[BDArmory.HitPointTracker] Setting HP of {part.name} to {Hitpoints}, destroying"); + DestroyPart(); + } + } + + public void AddDamage(float partdamage, bool overcharge = false) + { + if (isAI) return; + if (ArmorPanel) + { + if (BDArmorySettings.DEBUG_ARMOR) Debug.Log("[BDArmory.HitPointTracker] AddDamage(), hit part is armor panel, returning"); + return; + } + + partdamage = Mathf.Max(partdamage, 0f) * -1; + Hitpoints += (partdamage / defenseMutator); //why not just go -= partdamage? + if (BDArmorySettings.BATTLEDAMAGE && BDArmorySettings.BD_PART_STRENGTH) + { + part.breakingForce = maxForce * (Hitpoints / maxHitPoints); + part.breakingTorque = maxTorque * (Hitpoints / maxHitPoints); + part.gTolerance = maxG * (Hitpoints / maxHitPoints); + } + if (Hitpoints <= 0) + { + DestroyPart(); + } + } + + public void AddHealth(float partheal, bool overcharge = false) + { + if (isAI) return; + if (Hitpoints + partheal < BDArmorySettings.HEART_BLEED_THRESHOLD) //in case of negative regen value (for HP drain) + { + return; + } + Hitpoints += partheal; + + Hitpoints = Mathf.Clamp(Hitpoints, -1, overcharge ? Mathf.Min(previousHitpoints * 2, previousHitpoints + 1000) : previousHitpoints); //Allow vampirism to overcharge HP + } + + public void AddDamageToKerbal(KerbalEVA kerbal, float damage) + { + damage = Mathf.Max(damage, 0f) * -1; + Hitpoints += damage; + + if (Hitpoints <= 0) + { + // oh the humanity! + PartExploderSystem.AddPartToExplode(kerbal.part); + } + } + #endregion Hitpoints Functions + + #region Armor + + public void ReduceArmor(float massToReduce) //incoming massToreduce should be cm3 + { + if (BDArmorySettings.DEBUG_ARMOR) + { + Debug.Log("[HPTracker] armor mass: " + armorMass + "; mass to reduce: " + (massToReduce * Math.Round((Density / 1000000), 3)) * BDArmorySettings.ARMOR_MASS_MOD + "kg"); //g/m3 + } + float reduceMass = (massToReduce * (Density / 1000000000)); //g/cm3 conversion to yield tons + if (totalArmorQty > 0) + { + //Armor -= ((reduceMass * 2) / armorMass) * Armor; //armor that's 50% air isn't going to stop anything and could be considered 'destroyed' so lets reflect that by doubling armor loss (this will also nerf armor panels from 'god-tier' to merely 'very very good' + Armor -= ((reduceMass * 1.5f) / totalArmorQty) * Armor; + if (Armor < 0) + { + Armor = 0; + ArmorRemaining = 0; + } + else ArmorRemaining = Armor / StartingArmor * 100; + Armour = Armor; + } + else + { + if (Armor < 0) + { + Armor = 0; + ArmorRemaining = 0; + Armour = Armor; + } + } + if (ArmorPanel) + { + Hitpoints = ArmorRemaining; // * armorVolume * 10; + if (Armor <= 0) + { + DestroyPart(); + } + } + totalArmorQty -= reduceMass; + armorMass = totalArmorQty * BDArmorySettings.ARMOR_MASS_MOD; //tons + if (armorMass <= 0) + { + armorMass = 0; + } + } + + public void overrideArmorSetFromConfig() + { + ArmorSet = true; + if (ArmorThickness > 10 || ArmorPanel) //primarily panels, but any thing that starts with more than default armor + { + startsArmored = true; + if (Armor > 10 && Armor != ArmorThickness) //if settings modified and loading in from craft file + { } + else + { + Armor = ArmorThickness; + } + /* + UI_FloatRange armortypes = (UI_FloatRange)Fields["ArmorTypeNum"].uiControlEditor; + armortypes.minValue = 2f; //prevent panels from being switched to "None" armor type + if (ArmorTypeNum == 1) + { + ArmorTypeNum = 2; + } + */ + } + if (maxSupportedArmor < 0) //hasn't been set in cfg + { + if (part.IsAero()) + { + maxSupportedArmor = 20; + } + else + { + maxSupportedArmor = ((partSize.x / 20) * 1000); //~62mm for Size1, 125mm for S2, 185mm for S3 + maxSupportedArmor /= 5; + maxSupportedArmor = Mathf.Round(maxSupportedArmor); + maxSupportedArmor *= 5; + } + if (ArmorThickness > 10 && ArmorThickness > maxSupportedArmor)//part has custom armor value, use that + { + maxSupportedArmor = ArmorThickness; + } + } + if (BDArmorySettings.DEBUG_ARMOR) + { + Debug.Log("[ARMOR] max supported armor for " + part.name + " is " + maxSupportedArmor); + } + //if maxSupportedArmor > 0 && < armorThickness, that's entirely the fault of the MM patcher + UI_FloatRange armorFieldFlight = (UI_FloatRange)Fields["Armor"].uiControlFlight; + armorFieldFlight.minValue = 0f; + armorFieldFlight.maxValue = maxSupportedArmor; + UI_FloatRange armorFieldEditor = (UI_FloatRange)Fields["Armor"].uiControlEditor; + armorFieldEditor.maxValue = maxSupportedArmor; + armorFieldEditor.minValue = 1f; + armorFieldEditor.onFieldChanged = ArmorModified; + part.RefreshAssociatedWindows(); + } + + public void ArmorSetup(BaseField field, object obj) + { + if (OldArmorType != ArmorTypeNum) + { + if ((ArmorTypeNum - 1) > ArmorInfo.armorNames.Count) //in case of trying to load a craft using a mod armor type that isn't installed and having a armorTypeNum larger than the index size + { + //ArmorTypeNum = 1; //reset to 'None' + ArmorTypeNum = ArmorInfo.armors.FindIndex(t => t.name == "None") + 1; + } + if (isAI || part.IsMissile() || BDArmorySettings.RESET_ARMOUR) + { + ArmorTypeNum = ArmorInfo.armors.FindIndex(t => t.name == "None") + 1; + } + armorInfo = ArmorInfo.armors[ArmorInfo.armorNames[(int)ArmorTypeNum - 1]]; //what does this return if armorname cannot be found (mod armor removed/not present in install?) + + //if (SelectedArmorType != ArmorInfo.armorNames[(int)ArmorTypeNum - 1]) //armor selection overridden by Editor widget + //{ + // armorInfo = ArmorInfo.armors[SelectedArmorType]; + // ArmorTypeNum = ArmorInfo.armors.FindIndex(t => t.name == SelectedArmorType); //adjust part's current armor setting to match + //} + guiArmorTypeString = armorInfo.name; //FIXME - Localize these + SelectedArmorType = armorInfo.name; + Density = armorInfo.Density; + Diffusivity = armorInfo.Diffusivity; + Ductility = armorInfo.Ductility; + Hardness = armorInfo.Hardness; + Strength = armorInfo.Strength; + SafeUseTemp = armorInfo.SafeUseTemp; + + vFactor = armorInfo.vFactor; + muParam1 = armorInfo.muParam1; + muParam2 = armorInfo.muParam2; + muParam3 = armorInfo.muParam3; + muParam1S = armorInfo.muParam1S; + muParam2S = armorInfo.muParam2S; + muParam3S = armorInfo.muParam3S; + HEEquiv = armorInfo.HEEquiv; + HEATEquiv = armorInfo.HEATEquiv; + + SetArmor(); + } + if (BDArmorySettings.LEGACY_ARMOR) + { + guiArmorTypeString = guiArmorTypeString = StringUtils.Localize("#LOC_BDArmory_Steel"); + SelectedArmorType = "Legacy Armor"; + Density = 7850; + Diffusivity = 48.5f; + Ductility = 0.15f; + Hardness = 1176; + Strength = 940; + + // Calculated using yield = 700 MPa and youngModulus = 200 GPA + vFactor = 9.47761748e-07f; + muParam1 = 0.656060636f; + muParam2 = 1.20190930f; + muParam3 = 1.77791929f; + muParam1S = 0.947031140f; + muParam2S = 1.55575776f; + muParam3S = 2.75371552f; + HEEquiv = 1f; + HEATEquiv = 1f; + + SafeUseTemp = 2500; + if (BDArmorySettings.DEBUG_ARMOR) + { + Debug.Log("[ARMOR] Armor of " + part.name + " reset by LEGACY_ARMOUR"); + } + } + else if (BDArmorySettings.RESET_ARMOUR) //don't reset armor panels + { + guiArmorTypeString = guiArmorTypeString = StringUtils.Localize("#LOC_BDArmory_WMWindow_NoneWeapon"); //"none" + SelectedArmorType = "None"; + Density = 2700; + Diffusivity = 237f; + Ductility = 0.6f; + Hardness = 300; + Strength = 200; + + // Calculated using yield = 110 MPa and youngModulus = 70 GPA + vFactor = 1.82712211e-06f; + muParam1 = 1.37732446f; + muParam2 = 2.04939008f; + muParam3 = 4.53333330f; + muParam1S = 1.92650831f; + muParam2S = 2.65274119f; + muParam3S = 7.37037039f; + HEEquiv = 0.1601427673f; + HEATEquiv = 0.5528789891f; + + SafeUseTemp = 993; + Armor = 10; + if (ArmorPanel) + { + ArmorTypeNum = ArmorInfo.armors.FindIndex(t => t.name == "Steel") + 1; + Armor = 25; + Density = 7850; + Diffusivity = 48.5f; + Ductility = 0.15f; + Hardness = 1176; + Strength = 940; + + // Calculated using yield = 700 MPa and youngModulus = 200 GPA + vFactor = 9.47761748e-07f; + muParam1 = 0.656060636f; + muParam2 = 1.20190930f; + muParam3 = 1.77791929f; + muParam1S = 0.947031140f; + muParam2S = 1.55575776f; + muParam3S = 2.75371552f; + } + else + { + Fields["Armor"].guiActiveEditor = false; + Fields["guiArmorTypeString"].guiActiveEditor = false; + Fields["guiArmorTypeString"].guiActive = false; + Fields["armorCost"].guiActiveEditor = false; + Fields["armorMass"].guiActiveEditor = false; + } + if (BDArmorySettings.DEBUG_ARMOR) + { + Debug.Log("[ARMOR] Armor of " + part.name + " reset to defaults by RESET_ARMOUR"); + } + } + var oldArmorMass = armorMass; + part.skinInternalConductionMult = skinskinConduction; //reset to .cfg value + part.skinSkinConductionMult = skinInternalConduction; //reset to .cfg value + part.skinMassPerArea = 1; //default value + armorMass = 0; + armorCost = 0; + totalArmorQty = 0; + if (ArmorTypeNum != (ArmorInfo.armors.FindIndex(t => t.name == "None") + 1) && (!BDArmorySettings.LEGACY_ARMOR || (!BDArmorySettings.RESET_ARMOUR || (BDArmorySettings.RESET_ARMOUR && ArmorThickness > 10)))) //don't apply cost/mass to None armor type + { + armorMass = (Armor / 1000) * armorVolume * Density / 1000; //armor mass in tons + armorCost = (Armor / 1000) * armorVolume * armorInfo.Cost; //armor cost, tons + + part.skinInternalConductionMult = skinInternalConduction * BDAMath.Sqrt(Diffusivity / 237); //how well does the armor allow external heat to flow into the part internals? + part.skinSkinConductionMult = skinskinConduction * BDAMath.Sqrt(Diffusivity / 237); //how well does the armor conduct heat to connected part skins? + part.skinMassPerArea = (Density / 1000) * ArmorThickness; + } + if (ArmorTypeNum == (ArmorInfo.armors.FindIndex(t => t.name == "None") + 1) && ArmorPanel) + { + armorMass = (Armor / 1000) * armorVolume * Density / 1000; + guiArmorTypeString = StringUtils.Localize("#LOC_BDArmory_Aluminium"); + SelectedArmorType = "None"; + armorCost = (Armor / 1000) * armorVolume * armorInfo.Cost; + part.skinInternalConductionMult = skinInternalConduction * BDAMath.Sqrt(Diffusivity / 237); //how well does the armor allow external heat to flow into the part internals? + part.skinSkinConductionMult = skinskinConduction * BDAMath.Sqrt(Diffusivity / 237); //how well does the armor conduct heat to connected part skins? + part.skinMassPerArea = (Density / 1000) * ArmorThickness; + } + totalArmorQty = armorMass; //grabbing a copy of unmodified armorMAss so it can be used in armorMass' place for armor reduction without having to un/re-modify the mass before and after armor hits + armorMass *= BDArmorySettings.ARMOR_MASS_MOD; + //part.RefreshAssociatedWindows(); //having this fire every time a change happens prevents sliders from being used. Add delay timer? + if (OldArmorType != ArmorTypeNum || oldArmorMass != armorMass) + { + if (BDArmorySettings.DEBUG_ARMOR) Debug.Log($"[BDArmory.HitpointTracker]: {part.name} updated armour mass {oldArmorMass}->{armorMass} or type {OldArmorType}->{ArmorTypeNum} at time {Time.time}"); + OldArmorType = ArmorTypeNum; + _updateMass = true; + part.UpdateMass(); + if (HighLogic.LoadedSceneIsEditor && EditorLogic.fetch != null) + GameEvents.onEditorShipModified.Fire(EditorLogic.fetch.ship); + } + _armorConfigured = true; + } + + public void SetArmor() + { + //if (isAI) return; //replace with newer implementation + if (BDArmorySettings.LEGACY_ARMOR || BDArmorySettings.RESET_ARMOUR) return; + if (part.IsMissile()) return; + if (ArmorTypeNum != (ArmorInfo.armors.FindIndex(t => t.name == "None") + 1) || ArmorPanel) + { + /* + UI_FloatRange armorFieldFlight = (UI_FloatRange)Fields["Armor"].uiControlFlight; + if (armorFieldFlight.maxValue != maxSupportedArmor) + { + armorReset = false; + armorFieldFlight.minValue = 0f; + armorFieldFlight.maxValue = maxSupportedArmor; + } + */ + Fields["Armor"].guiActiveEditor = true; + Fields["guiArmorTypeString"].guiActiveEditor = true; + Fields["guiArmorTypeString"].guiActive = true; + Fields["armorCost"].guiActiveEditor = true; + Fields["armorMass"].guiActiveEditor = true; + UI_FloatRange armorFieldEditor = (UI_FloatRange)Fields["Armor"].uiControlEditor; + if (armorFieldEditor.maxValue != maxSupportedArmor) + { + armorReset = false; + armorFieldEditor.maxValue = maxSupportedArmor; + armorFieldEditor.minValue = 1f; + } + armorFieldEditor.onFieldChanged = ArmorModified; + if (!armorReset) + { + part.RefreshAssociatedWindows(); + } + armorReset = true; + } + else + { + Armor = 10; + Fields["Armor"].guiActiveEditor = false; + Fields["guiArmorTypeString"].guiActiveEditor = false; + Fields["guiArmorTypeString"].guiActive = false; + Fields["armorCost"].guiActiveEditor = false; + Fields["armorMass"].guiActiveEditor = false; + //UI_FloatRange armorFieldEditor = (UI_FloatRange)Fields["Armor"].uiControlEditor; + //armorFieldEditor.maxValue = 10; //max none armor to 10 (simulate part skin of alimunium) + //armorFieldEditor.minValue = 10; + + part.RefreshAssociatedWindows(); + //GameEvents.onEditorShipModified.Fire(EditorLogic.fetch.ship); + } + } + private static Bounds CalcPartBounds(Part p, Transform t) + { + Bounds result = new Bounds(t.position, Vector3.zero); + Bounds[] bounds = p.GetRendererBounds(); //slower than getColliderBounds, but it only runs once, and doesn't have to deal with culling isTrgger colliders (airlocks, ladders, etc) + //Err... not so sure about that, me. This is yielding different resutls in SPH/flight. SPH is proper dimensions, flight is giving bigger x/y/z + // a mk1 cockpit (x: 1.25, y: 1.6, z: 1.9, area 11 in SPh becomes x: 2.5, y: 1.25, z: 2.5, area 19 + { + if (!p.Modules.Contains("LaunchClamp")) + { + for (int i = 0; i < bounds.Length; i++) + { + result.Encapsulate(bounds[i]); + } + } + } + return result; + } + + public void HullSetup(BaseField field, object obj) //no longer needed for realtime HP calcs, but does need to be updated occasionally to give correct vessel mass + { + if (isProcWing) + { + StartCoroutine(WaitForHullSetup()); + } + else + { + SetHullMass(); + } + } + IEnumerator WaitForHullSetup() + { + if (waitingForHullSetup) yield break; // Already waiting. + waitingForHullSetup = true; + yield return new WaitForFixedUpdate(); + waitingForHullSetup = false; + if (part == null) yield break; // The part disappeared! + + SetHullMass(); + } + void SetHullMass() + { + if (IgnoreForArmorSetup) + { + _hullConfigured = true; + return; + } + if (isAI || ArmorPanel || ProjectileUtils.isMaterialBlackListpart(this.part)) + { + _hullConfigured = true; + return; + //HullTypeNum = HullInfo.materials.FindIndex(t => t.name == "Aluminium"); + } + + if (OldHullType != HullTypeNum || (BDArmorySettings.RESET_HULL || BDArmorySettings.LEGACY_ARMOR)) + + { + if ((HullTypeNum - 1) > HullInfo.materialNames.Count || (BDArmorySettings.RESET_HULL || BDArmorySettings.LEGACY_ARMOR)) //in case of trying to load a craft using a mod hull type that isn't installed and having a hullTypeNum larger than the index size + { + if (!HullInfo.materialNames.Contains("Aluminium")) Debug.LogError("[BDArmory.HitpointTracker] BD_Materials.cfg missing! Please fix your BDA insteall"); + HullTypeNum = HullInfo.materials.FindIndex(t => t.name == "Aluminium") + 1; + } + + if ((part.isEngine() || part.IsWeapon()) && HullInfo.materials[HullInfo.materialNames[(int)HullTypeNum - 1]].massMod < 1) //can armor engines, but not make them out of wood. + { + HullTypeNum = HullInfo.materials.FindIndex(t => t.name == "Aluminium") + 1; + part.maxTemp = part.partInfo.partPrefab.maxTemp; + } + + hullInfo = HullInfo.materials[HullInfo.materialNames[(int)HullTypeNum - 1]]; + } + var OldHullMassAdjust = HullMassAdjust; + HullMassAdjust = (partMass * hullInfo.massMod) - partMass; + guiHullTypeString = String.IsNullOrEmpty(hullInfo.localizedName) ? hullInfo.name : StringUtils.Localize(hullInfo.localizedName); + if (hullInfo.maxTemp > 0) + { + part.maxTemp = hullInfo.maxTemp; + part.skinMaxTemp = hullInfo.maxTemp; + } + else + { + part.maxTemp = part.partInfo.partPrefab.maxTemp > 0 ? part.partInfo.partPrefab.maxTemp : 2500; //kerbal flags apparently starting with -1 maxtemp + part.skinMaxTemp = part.partInfo.partPrefab.skinMaxTemp > 0 ? part.partInfo.partPrefab.skinMaxTemp : 2500; + } + ignitionTemp = hullInfo.ignitionTemp; + part.crashTolerance = part.partInfo.partPrefab.crashTolerance * hullInfo.ImpactMod; + maxForce = part.partInfo.partPrefab.breakingForce * hullInfo.ImpactMod; + part.breakingForce = maxForce; + maxTorque = part.partInfo.partPrefab.breakingTorque * hullInfo.ImpactMod; + part.breakingTorque = maxTorque; + maxG = part.partInfo.partPrefab.gTolerance * hullInfo.ImpactMod; + part.gTolerance = maxG; + hullType = hullInfo.name; + float partCost = part.partInfo.cost + part.partInfo.variant.Cost; + if (hullInfo.costMod < 1) HullCostAdjust = Mathf.Max((partCost - (float)resourceCost) * hullInfo.costMod, partCost - (1000 - (hullInfo.costMod * 1000))) - (partCost - (float)resourceCost);//max of 1000 funds discount on cheaper materials + else HullCostAdjust = Mathf.Min((partCost - (float)resourceCost) * hullInfo.costMod, (partCost - (float)resourceCost) + (hullInfo.costMod * 1000)) - (partCost - (float)resourceCost); //Increase costs if costMod => 1 + //this returns cost of base variant, yielding part variant that are discounted by 50% or 500 of base variant cost, not current variant. method to get currently selected variant? + + if (OldHullType != HullTypeNum || OldHullMassAdjust != HullMassAdjust) + { + if (BDArmorySettings.DEBUG_ARMOR) Debug.Log($"[BDArmory.HitpointTracker]: {part.name} updated hull mass {OldHullMassAdjust}->{HullMassAdjust} (part mass {partMass}, total mass {part.mass + HullMassAdjust - OldHullMassAdjust}) or type {OldHullType}->{HullTypeNum} at time {Time.time}"); + OldHullType = HullTypeNum; + _updateMass = true; + part.UpdateMass(); + if (HighLogic.LoadedSceneIsEditor && EditorLogic.fetch != null) + GameEvents.onEditorShipModified.Fire(EditorLogic.fetch.ship); + } + _hullConfigured = true; + } + private List GetResources() + { + List resources = new List(); + + foreach (PartResource resource in part.Resources) + { + if (!resources.Contains(resource)) { resources.Add(resource); } + } + return resources; + } + private void CalculateDryCost() + { + resourceCost = 0; + foreach (PartResource resource in GetResources()) + { + var resources = part.Resources.ToList(); + using (IEnumerator res = resources.GetEnumerator()) + while (res.MoveNext()) + { + if (res.Current == null) continue; + if (res.Current.resourceName == resource.resourceName) + { + resourceCost += res.Current.info.unitCost * res.Current.maxAmount; //turns out parts subtract res cost even if the tank starts empty + } + } + } + } + #endregion Armor + public override string GetInfo() + { + StringBuilder output = new StringBuilder(); + output.Append(Environment.NewLine); + if (startsArmored || ArmorPanel) + { + output.AppendLine($"Starts Armored"); + output.AppendLine($" - Armor Mass: {armorMass}"); + } + return output.ToString(); + } + } +} diff --git a/BDArmory.Core/Services/ModuleDamageService.cs b/BDArmory/Damage/ModuleDamageService.cs similarity index 96% rename from BDArmory.Core/Services/ModuleDamageService.cs rename to BDArmory/Damage/ModuleDamageService.cs index a15796ad1..c10cf63b2 100644 --- a/BDArmory.Core/Services/ModuleDamageService.cs +++ b/BDArmory/Damage/ModuleDamageService.cs @@ -1,10 +1,15 @@ -using BDArmory.Core.Enum; -using BDArmory.Core.Events; -using BDArmory.Core.Module; -using UnityEngine; +using UnityEngine; -namespace BDArmory.Core.Services +using BDArmory.Services; + +namespace BDArmory.Damage { + public enum DamageOperation + { + Set = 0, + Add = 1 + } + internal class ModuleDamageService : DamageService { public override void ReduceArmor_svc(Part p, float armorMass) diff --git a/BDArmory/Modules/ModuleDrainEC.cs b/BDArmory/Damage/ModuleDrainEC.cs similarity index 87% rename from BDArmory/Modules/ModuleDrainEC.cs rename to BDArmory/Damage/ModuleDrainEC.cs index 6934fcb75..a949c2a23 100644 --- a/BDArmory/Modules/ModuleDrainEC.cs +++ b/BDArmory/Damage/ModuleDrainEC.cs @@ -1,12 +1,17 @@ -using BDArmory.Competition; -using BDArmory.Control; -using BDArmory.Core; -using BDArmory.UI; -using System.Collections; +using System.Collections; using System.Linq; using UnityEngine; -namespace BDArmory.Modules +using BDArmory.Competition; +using BDArmory.Control; +using BDArmory.Radar; +using BDArmory.Settings; +using BDArmory.Targeting; +using BDArmory.UI; +using BDArmory.Utils; +using BDArmory.Weapons; + +namespace BDArmory.Damage { public class ModuleDrainEC : PartModule { @@ -18,6 +23,8 @@ public class ModuleDrainEC : PartModule private bool disabled = false; //prevent further EMP buildup while rebooting public bool bricked = false; //He's dead, jeb private float rebootTimer = 15; + private bool initialAIState = false; //if for whatever reason players are manually firing EMPs at targets with AI/WM disabled, don't enable them when vessel reboots + private bool initialWMState = false; private void EnableVessel() { @@ -38,28 +45,27 @@ private void EnableVessel() var weapon = p.FindModuleImplementing(); if (weapon != null) { - //weapon.weaponState = ModuleWeapon.WeaponStates.Disabled; //allow weapons to be used again - weapon.DisableWeapon(); if (weapon.isAPS) - { - //weapon.weaponState = ModuleWeapon.WeaponStates.Enabled; //allow weapons to be used again - weapon.EnableWeapon(); - } + weapon.EnableWeapon(); //reactivate APS + else + weapon.DisableWeapon(); //reset WeaponState } if (command != null) { command.minimumCrew /= 10; //more elegant than a dict storing every crew part's cap to restore to original amount } var AI = p.FindModuleImplementing(); - if (AI != null) + if (AI != null && initialAIState) { AI.ActivatePilot(); //It's Alive! + initialAIState = false; } var WM = p.FindModuleImplementing(); - if (WM != null) + if (WM != null && initialWMState) { WM.guardMode = true; WM.debilitated = false; + initialWMState = false; } } vessel.ActionGroups.ToggleGroup(KSPActionGroup.Custom10); // restart engines @@ -71,7 +77,7 @@ private void EnableVessel() disabled = false; } - void Update() + void FixedUpdate() { if (!HighLogic.LoadedSceneIsFlight) return; if (BDArmorySetup.GameIsPaused) return; @@ -120,7 +126,7 @@ void UpdateEMPLevel() { disabled = true; //if so disable the craft var message = "Disabling " + vessel.vesselName + " for " + rebootTimer + "s due to EMP damage"; - Debug.Log("[BDArmory.ModuleDrainEC]: " + message); + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log("[BDArmory.ModuleDrainEC]: " + message); BDACompetitionMode.Instance.competitionStatus.Add(message); DisableVessel(); } @@ -128,13 +134,13 @@ void UpdateEMPLevel() { bricked = true; //if so brick the craft var message = vessel.vesselName + " is bricked!"; - Debug.Log("[BDArmory.ModuleDrainEC]: " + message); + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log("[BDArmory.ModuleDrainEC]: " + message); BDACompetitionMode.Instance.competitionStatus.Add(message); } if (EMPDamage <= 0 && disabled && !bricked) //reset craft { var message = "Rebooting " + vessel.vesselName; - Debug.Log("[BDArmory.ModuleDrainEC]: " + message); + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log("[BDArmory.ModuleDrainEC]: " + message); BDACompetitionMode.Instance.competitionStatus.Add(message); EnableVessel(); } @@ -200,11 +206,13 @@ private void DisableVessel() var AI = p.FindModuleImplementing(); if (AI != null) { + if (AI.pilotEnabled) initialAIState = true; AI.DeactivatePilot(); //disable AI } var WM = p.FindModuleImplementing(); if (WM != null) { + if (WM.guardMode) initialWMState = true; WM.guardMode = false; //disable guardmode WM.debilitated = true; //for weapon selection and targeting; } @@ -241,7 +249,7 @@ public void Start() } IEnumerator TimerRoutine() { - yield return new WaitForSeconds(5); + yield return new WaitForSecondsFixed(5); Destroy(gameObject); } diff --git a/BDArmory/Modules/ModuleDrainIntakes.cs b/BDArmory/Damage/ModuleDrainIntakes.cs similarity index 95% rename from BDArmory/Modules/ModuleDrainIntakes.cs rename to BDArmory/Damage/ModuleDrainIntakes.cs index 8dad20d10..aa2c7f13b 100644 --- a/BDArmory/Modules/ModuleDrainIntakes.cs +++ b/BDArmory/Damage/ModuleDrainIntakes.cs @@ -1,7 +1,8 @@ using UnityEngine; -using System.Collections.Generic; -namespace BDArmory.Modules +using BDArmory.Utils; + +namespace BDArmory.Damage { public class ModuleDrainIntakes : PartModule { diff --git a/BDArmory/Modules/ModuleMassAdjust.cs b/BDArmory/Damage/ModuleMassAdjust.cs similarity index 82% rename from BDArmory/Modules/ModuleMassAdjust.cs rename to BDArmory/Damage/ModuleMassAdjust.cs index 7393c270a..37e74a2d4 100644 --- a/BDArmory/Modules/ModuleMassAdjust.cs +++ b/BDArmory/Damage/ModuleMassAdjust.cs @@ -1,10 +1,9 @@ -using BDArmory.Control; -using BDArmory.UI; -using System.Collections; -using System.Linq; using UnityEngine; -namespace BDArmory.Modules +using BDArmory.UI; +using BDArmory.Settings; + +namespace BDArmory.Damage { public class ModuleMassAdjust : PartModule, IPartMassModifier { @@ -23,12 +22,12 @@ private void EndEffect() //Debug.Log("[BDArmory.ModuleMassAdjust]: ME field expired, " + this.part.name + "mass: " + this.part.mass); } - void Update() + void FixedUpdate() { if (!HighLogic.LoadedSceneIsFlight) return; if (BDArmorySetup.GameIsPaused) return; - duration -= 1 * TimeWarp.fixedDeltaTime; + duration -= TimeWarp.fixedDeltaTime; if (duration <= 0) { @@ -44,7 +43,7 @@ private void SetupME() { startMass = this.part.mass; hasSetup = true; - Debug.Log("[BDArmory.ModuleMassAdjust]: Applying ME field to " + this.part.name + ", orig mass: " + startMass + ", massMod = " + massMod); + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log("[BDArmory.ModuleMassAdjust]: Applying ME field to " + this.part.name + ", orig mass: " + startMass + ", massMod = " + massMod); if (massMod < 0) //for negative mass modifier - i.e. MassEffect sytyle antigrav/weight reduction { diff --git a/BDArmory/Distribution/GameData/BDArmory/BDArmory.version b/BDArmory/Distribution/GameData/BDArmory/BDArmory.version index 751783fde..941345b93 100644 --- a/BDArmory/Distribution/GameData/BDArmory/BDArmory.version +++ b/BDArmory/Distribution/GameData/BDArmory/BDArmory.version @@ -1,25 +1,25 @@ { "NAME":"BDArmory", - "URL":"https://github.com/PapaJoesSoup/BDArmory/raw/master/BDArmory/Distribution/GameData/BDArmory/BDArmory.version", - "DOWNLOAD":"https://github.com/PapaJoesSoup/BDArmory/releases/latest", + "URL":"https://github.com/BrettRyland/BDArmory/raw/master/BDArmory/Distribution/GameData/BDArmory/BDArmory.version", + "DOWNLOAD":"https://github.com/BrettRyland/BDArmory/releases/latest", "GITHUB": { - "USERNAME":"PapaJoesSoup", + "USERNAME":"BrettRyland", "REPOSITORY":"BDArmory", "ALLOW_PRE_RELEASE":false }, "VERSION": { "MAJOR":1, - "MINOR":4, - "PATCH":18, - "BUILD":4 + "MINOR":6, + "PATCH":3, + "BUILD":0 }, "KSP_VERSION": { "MAJOR":1, "MINOR":12, - "PATCH":3 + "PATCH":5 }, "KSP_VERSION_MIN": { diff --git a/BDArmory/Distribution/GameData/BDArmory/BulletDefs/BD_Armors.cfg b/BDArmory/Distribution/GameData/BDArmory/BulletDefs/BD_Armors.cfg index 339f14f03..7c7219816 100644 --- a/BDArmory/Distribution/GameData/BDArmory/BulletDefs/BD_Armors.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/BulletDefs/BD_Armors.cfg @@ -8,6 +8,8 @@ ARMOR Density = 7850 //in kg/m3 Strength = 840 // Ultimate Tensile Strengh, in MPa Hardness = 1176 // in MPa, using Brinell + Yield = 700 // Yield Strength, in MPa + YoungModulus = 200 // Young Modulus, in GPa Ductility = 0.15 //measure of deformation/elongation; 0 is Ceramic, 1 is rubber Diffusivity = 48.8 //ability to diffuse thermal energy - laser resist SafeUseTemp = 1694 // in Kelvin, safe-use temperature. Above this temp strength decreases @@ -23,17 +25,22 @@ ARMOR Density = 2700 //hardcoded overide, armor of type None will add 0 mass to part Strength = 200 Hardness = 300 + Yield = 110 + YoungModulus = 70 Ductility = 0.60 Diffusivity = 237 SafeUseTemp = 993 Cost = 400 } + ARMOR { name = Mild Steel //more or less analogous to legacy armor Density = 7850 Strength = 940 Hardness = 1176 + Yield = 700 + YoungModulus = 200 Ductility = 0.15 Diffusivity = 48.8 SafeUseTemp = 1694 @@ -46,10 +53,12 @@ ARMOR Density = 4506 Strength = 552 Hardness = 2000 + Yield = 330 + YoungModulus = 116 Ductility = 0.58 Diffusivity = 22 SafeUseTemp = 703 //Titanium loses tensile strength above 430 C - Cost = 5000 + Cost = 5000 } ARMOR @@ -58,6 +67,8 @@ ARMOR Density = 1850 Strength = 370 Hardness = 1320 + Yield = 240 + YoungModulus = 287 Ductility = 0.07 Diffusivity = 200 SafeUseTemp = 1830 @@ -68,8 +79,10 @@ ARMOR { name = Aramid Fibre Density = 1440 - Strength = 3757 + Strength = 300 Hardness = 10 + Yield = 300 + YoungModulus = 82.2 Ductility = 0.035 Diffusivity = 0.04 SafeUseTemp = 770 @@ -79,9 +92,11 @@ ARMOR ARMOR { name = S-Glass Composite - Density = 1950 //2480 for raw S-Glass fibre - Strength = 2358 //4710 for raw fibre + Density = 1800 //2480 for raw S-Glass fibre + Strength = 274.6 //4710 for raw fibre Hardness = 780 + Yield = 274.6 + YoungModulus = 93 Ductility = 0.015 //will fail catastrophically if stressed past tolerance //0.98 Y/T Diffusivity = 1.35 SafeUseTemp = 470 //resins vulnerable to heating, 1470 for raw fibre @@ -94,8 +109,24 @@ ARMOR Density = 19000 Strength = 1720 Hardness = 3850 + Yield = 965 + YoungModulus = 170 Ductility = 0.15 Diffusivity = 12 SafeUseTemp = 1623 Cost = 21000 } + +ARMOR +{ + name = Armor Aluminium // Aluminium 7039 + Density = 2740 + Strength = 450 + Hardness = 300 + Yield = 380 + YoungModulus = 69.6 + Ductility = 0.12 + Diffusivity = 58 + SafeUseTemp = 644 + Cost = 600 +} diff --git a/BDArmory/Distribution/GameData/BDArmory/BulletDefs/BD_Bullets.cfg b/BDArmory/Distribution/GameData/BDArmory/BulletDefs/BD_Bullets.cfg index b83731298..91e4a8b8a 100644 --- a/BDArmory/Distribution/GameData/BDArmory/BulletDefs/BD_Bullets.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/BulletDefs/BD_Bullets.cfg @@ -13,7 +13,7 @@ BULLET apBulletMod = 1 //Armor penetration depth multiplier subProjectileCount = 1 //projectiles fired per triggerpull, for shotguns/etc //Bullet stats - explosive = True //round explodes + explosive = Standard //choose from Standard or Shaped, or None for non-explosive rounds incendiary = False //round starts fires EMP = false //inflicts EMP buildup nuclear = false //nuclear shell, uses tntMass for yield(kt) @@ -40,7 +40,7 @@ BULLET bulletVelocity = 718 bulletMass = 0.0965 //HE Bullet Values - explosive = False + explosive = None incendiary = False tntMass = 0 fuzeType = None @@ -58,9 +58,9 @@ BULLET DisplayName = .303 British caliber = 7.7 bulletVelocity = 825 - bulletMass = 0.0975 + bulletMass = 0.00975 //HE Bullet Values - explosive = False + explosive = None incendiary = False tntMass = 0 fuzeType = None @@ -68,7 +68,7 @@ BULLET fadeColor = False startColor = 145, 249, 160, 120 subProjectileCount = 1 - apBulletMod = 1 + apBulletMod = 1 //15mm penetration steel bulletDragTypeName = AnalyticEstimate } @@ -78,9 +78,9 @@ BULLET DisplayName = 7.92 Mauser caliber = 7.92 bulletVelocity = 825 - bulletMass = 0.1 + bulletMass = 0.01 //HE Bullet Values - explosive = False + explosive = None incendiary = False tntMass = 0 fuzeType = None @@ -88,7 +88,7 @@ BULLET fadeColor = False startColor = 222, 249, 242, 120 subProjectileCount = 1 - apBulletMod = 1 + apBulletMod = 1 //15mm bulletDragTypeName = AnalyticEstimate } @@ -100,7 +100,7 @@ BULLET bulletVelocity = 380 bulletMass = 0.114 //HE Bullet Values - explosive = False + explosive = None incendiary = False tntMass = 0 fuzeType = None @@ -108,7 +108,7 @@ BULLET fadeColor = False startColor = 212, 145, 2, 120 subProjectileCount = 1 - apBulletMod = 1 + apBulletMod = 1 //11mm bulletDragTypeName = AnalyticEstimate } @@ -119,11 +119,11 @@ BULLET DisplayName = Raufoss Mk211 caliber = 12.7 bulletVelocity = 915 - bulletMass = .15 - explosive = True + bulletMass = .06 + explosive = Standard incendiary = False tntMass = .01 - apBulletMod = 1 + apBulletMod = 1 //should be 15.6mm at 1000m bulletDragTypeName = AnalyticEstimate subProjectileCount = 1 fuzeType = Impact @@ -140,10 +140,10 @@ BULLET bulletVelocity = 890 bulletMass = .057 //HE Bullet Values - explosive = False + explosive = None incendiary = True tntMass = 0 - apBulletMod = 1.2 + apBulletMod = 1.2 //36mm bulletDragTypeName = AnalyticEstimate subProjectileCount = 1 fuzeType = None @@ -160,10 +160,10 @@ BULLET bulletVelocity = 890 bulletMass = .052 //HE Bullet Values - explosive = True + explosive = Standard incendiary = False tntMass = .008 - apBulletMod = 0.8 + apBulletMod = 0.8 //23mm bulletDragTypeName = AnalyticEstimate subProjectileCount = 1 fuzeType = Impact @@ -180,7 +180,7 @@ BULLET bulletVelocity = 880 bulletMass = 0.168 //HE Bullet Values - explosive = False + explosive = None incendiary = False tntMass = 0 fuzeType = None @@ -188,7 +188,7 @@ BULLET fadeColor = False startColor = 128, 128, 128, 0 subProjectileCount = 1 - apBulletMod = 1 + apBulletMod = 1 //40mm bulletDragTypeName = AnalyticEstimate } @@ -200,7 +200,7 @@ BULLET bulletVelocity = 810 bulletMass = 0.095 //HE Bullet Values - explosive = True + explosive = Standard incendiary = False tntMass = 0.09 fuzeType = Impact @@ -208,7 +208,7 @@ BULLET fadeColor = False startColor = 128, 128, 128, 0 subProjectileCount = 1 - apBulletMod = 0.8 + apBulletMod = 0.46 //11mm bulletDragTypeName = AnalyticEstimate } @@ -217,10 +217,10 @@ BULLET name = 20x102mmBullet //Vulcan API DisplayName = 20mm API caliber = 20 - bulletVelocity = 1050 + bulletVelocity = 1030 bulletMass = 0.1101 //HE Bullet Values - explosive = False + explosive = None incendiary = True tntMass = 0 fuzeType = None @@ -228,7 +228,7 @@ BULLET fadeColor = False startColor = 255, 105, 0, 64 subProjectileCount = 1 - apBulletMod = 1 + apBulletMod = 1 //38mm //6.4mm pen at 1000m bulletDragTypeName = AnalyticEstimate } @@ -237,10 +237,30 @@ BULLET name = 20x102mmHEBullet //Vulcan HE DisplayName = 20mm HE caliber = 20 + bulletVelocity = 1030 + bulletMass = 0.1 + //HE Bullet Values + explosive = Standard + incendiary = False + tntMass = 0.015 // originally 0.0625 + fuzeType = Impact + projectileColor = 255, 15, 0, 128 + fadeColor = False + startColor = 128, 128, 128, 0 + subProjectileCount = 1 + apBulletMod = 0.37 // 12.7mm + bulletDragTypeName = AnalyticEstimate +} + +BULLET +{ + name = 20x102mmSAPHEIBullet //Vulcan PGU-28/B + DisplayName = 20mm SAPHEI + caliber = 20 bulletVelocity = 1050 - bulletMass = 0.1101 + bulletMass = 0.1024 //HE Bullet Values - explosive = True + explosive = Standard incendiary = False tntMass = 0.015 // originally 0.0625 fuzeType = Impact @@ -248,7 +268,7 @@ BULLET fadeColor = False startColor = 128, 128, 128, 0 subProjectileCount = 1 - apBulletMod = 0.9 + apBulletMod = 0.75 //28mm bulletDragTypeName = AnalyticEstimate } @@ -260,7 +280,7 @@ BULLET bulletVelocity = 950 bulletMass = 0.1101 //HE Bullet Values - explosive = False + explosive = None incendiary = False EMP = True tntMass = 0 @@ -269,7 +289,7 @@ BULLET fadeColor = False startColor = 128, 128, 128, 0 subProjectileCount = 1 - apBulletMod = 0.9 + apBulletMod = 0.1 bulletDragTypeName = AnalyticEstimate } @@ -281,7 +301,7 @@ BULLET bulletVelocity = 950 bulletMass = 0.1101 //HE Bullet Values - explosive = False + explosive = None incendiary = False massMod = 0.1 impulse = 2000 @@ -291,7 +311,7 @@ BULLET fadeColor = False startColor = 128, 128, 128, 0 subProjectileCount = 1 - apBulletMod = 0.9 + apBulletMod = 0.1 bulletDragTypeName = AnalyticEstimate } BULLET @@ -302,7 +322,7 @@ BULLET bulletVelocity = 720 bulletMass = 0.1900 //HE Bullet Values - explosive = True + explosive = Standard incendiary = False tntMass = 0.2534 fuzeType = Impact @@ -310,7 +330,7 @@ BULLET fadeColor = False startColor = 245, 35, 2, 120 subProjectileCount = 1 - apBulletMod = 0.8 + apBulletMod = 1 //25mm bulletDragTypeName = AnalyticEstimate } @@ -322,7 +342,7 @@ BULLET bulletVelocity = 1020 bulletMass = 0.19 //HE Bullet Values - explosive = True + explosive = Standard incendiary = False tntMass = 0.2534 fuzeType = Impact @@ -330,7 +350,7 @@ BULLET fadeColor = False startColor = 245, 35, 2, 120 subProjectileCount = 1 - apBulletMod = 0.8 + apBulletMod = 0.8 //34mm bulletDragTypeName = AnalyticEstimate } @@ -342,7 +362,7 @@ BULLET bulletVelocity = 1020 bulletMass = 0.195 //HE Bullet Values - explosive = False + explosive = None incendiary = False tntMass = 0 fuzeType = None @@ -350,7 +370,7 @@ BULLET fadeColor = False startColor = 245, 245, 21, 120 subProjectileCount = 1 - apBulletMod = 1 + apBulletMod = 1 //41mm bulletDragTypeName = AnalyticEstimate } @@ -362,7 +382,7 @@ BULLET bulletVelocity = 720 bulletMass = 0.19 //HE Bullet Values - explosive = True + explosive = Standard incendiary = False tntMass = 0.2534 fuzeType = Impact @@ -370,7 +390,7 @@ BULLET fadeColor = False startColor = 245, 35, 2, 120 subProjectileCount = 1 - apBulletMod = 0.8 + apBulletMod = 0.8 //19mm bulletDragTypeName = AnalyticEstimate } @@ -382,7 +402,7 @@ BULLET bulletVelocity = 1020 bulletMass = 0.19 //HE Bullet Values - explosive = True + explosive = Standard incendiary = True tntMass = 0.05 //Originally 0.2534 fuzeType = Impact @@ -390,7 +410,7 @@ BULLET fadeColor = False startColor = 245, 35, 2, 120 subProjectileCount = 1 - apBulletMod = 0.8 + apBulletMod = 0.8 //33mm bulletDragTypeName = AnalyticEstimate } @@ -402,7 +422,7 @@ BULLET bulletVelocity = 970 bulletMass = 0.223 //HE Bullet Values - explosive = True + explosive = Standard incendiary = False tntMass = 0.05 fuzeType = Impact @@ -410,7 +430,7 @@ BULLET fadeColor = False startColor = 245, 35, 2, 120 subProjectileCount = 1 - apBulletMod = 1.1 + apBulletMod = 0.95 //40mm bulletDragTypeName = AnalyticEstimate } @@ -422,7 +442,7 @@ BULLET bulletVelocity = 1020 bulletMass = 0.19 //HE Bullet Values - explosive = True + explosive = Standard incendiary = False tntMass = 0.1 fuzeType = Flak @@ -442,7 +462,7 @@ BULLET bulletVelocity = 540 bulletMass = 0.33 //HE Bullet Values - explosive = True + explosive = Standard incendiary = False tntMass = 0.12 fuzeType = Impact @@ -450,7 +470,7 @@ BULLET fadeColor = False startColor = 128, 128, 128, 120 subProjectileCount = 1 - apBulletMod = 1 + apBulletMod = 0.87 //15mm bulletDragTypeName = AnalyticEstimate } @@ -462,7 +482,7 @@ BULLET bulletVelocity = 870 bulletMass = 0.3880 //HE Bullet Values - explosive = False + explosive = None incendiary = False tntMass = 0 fuzeType = None @@ -470,7 +490,7 @@ BULLET fadeColor = True startColor = 255, 255, 0, 120 subProjectileCount = 1 - apBulletMod = 1 + apBulletMod = 1 //41mm bulletDragTypeName = AnalyticEstimate } @@ -482,7 +502,7 @@ BULLET bulletVelocity = 1109 bulletMass = 0.3880 //HE Bullet Values - explosive = False + explosive = None incendiary = True //sure, why not? tntMass = 0 fuzeType = None @@ -490,7 +510,7 @@ BULLET fadeColor = False startColor = 128, 128, 128, 120 subProjectileCount = 1 - apBulletMod = 1.1 + apBulletMod = 1.1 //65mm bulletDragTypeName = AnalyticEstimate } @@ -503,7 +523,7 @@ BULLET bulletVelocity = 1109 bulletMass = 0.3880 //HE Bullet Values - explosive = True + explosive = Standard incendiary = False tntMass = 0.077 // originally 0.254 fuzeType = Impact @@ -511,7 +531,7 @@ BULLET fadeColor = True startColor = 255, 30, 0, 32 subProjectileCount = 1 - apBulletMod = 0.8 + apBulletMod = 0.8 //47mm bulletDragTypeName = AnalyticEstimate } @@ -523,7 +543,7 @@ BULLET bulletVelocity = 1109 bulletMass = 0.3880 //HE Bullet Values - explosive = True + explosive = Standard incendiary = False tntMass = 0.0770 // originally 0.254 fuzeType = Timed @@ -531,50 +551,70 @@ BULLET fadeColor = True startColor = 255, 30, 0, 32 subProjectileCount = 1 - apBulletMod = 0.8 + apBulletMod = 0.8 //47 bulletDragTypeName = AnalyticEstimate } - BULLET { name = 35x228HEBullet //oerlikon - DisplayName = Oerlikon AHEAD + DisplayName = Oerlikon HE caliber = 35 bulletVelocity = 1175 + bulletMass = 0.51 + //HE Bullet Values + explosive = Standard + incendiary = False + tntMass = 0.098 + fuzeType = Impact + projectileColor = 255, 250, 0, 128 + fadeColor = True + startColor = 255, 120, 0, 32 + subProjectileCount = 1 + apBulletMod = 0.8 //55 + bulletDragTypeName = AnalyticEstimate +} +BULLET +{ + name = 35x228AHEADBullet //oerlikon + DisplayName = Oerlikon AHEAD + caliber = 35 + bulletVelocity = 1100 bulletMass = 0.550 //HE Bullet Values - explosive = True + explosive = Standard incendiary = False beehive = True - subMunitionType = 35x228AHEADBullet - tntMass = 0.098 + subMunitionType = 35x228AHEADPellet + tntMass = 0.01 fuzeType = Timed projectileColor = 255, 250, 0, 128 fadeColor = True startColor = 255, 240, 0, 32 subProjectileCount = 1 - apBulletMod = 0.8 + apBulletMod = 0.8 //55 bulletDragTypeName = AnalyticEstimate } BULLET { - name = 35x228AHEADBullet //oerlikon - DisplayName = ADEAD Submunition + name = 35x228AHEADPellet //oerlikon + DisplayName = AHEAD Submunition caliber = 10 - bulletVelocity = 1175 - bulletMass = 0.015 + bulletVelocity = 5 //this is added to parent bulletVelocity + bulletMass = 0.025 //HE Bullet Values - explosive = False + explosive = None incendiary = False tntMass = 0 fuzeType = None projectileColor = 144, 144, 144, 128 fadeColor = True startColor = 192, 192, 192, 32 - apBulletMod = 0.8 + apBulletMod = 0.8 //21mm bulletDragTypeName = AnalyticEstimate - subProjectileCount = 50 //IRL 152! + subProjectileCount = 30 //IRL 152! + subProjectileDispersion = 120 //influenced by bulletVel, lower bV needs higher sPD for similar spread + } BULLET @@ -585,7 +625,7 @@ BULLET bulletVelocity = 242 bulletMass = 0.3500 //HE Bullet Values - explosive = True + explosive = Standard incendiary = False tntMass = 0.25 fuzeType = Impact @@ -593,7 +633,7 @@ BULLET fadeColor = False startColor = 128, 128, 128, 0 subProjectileCount = 1 - apBulletMod = 0.8 + apBulletMod = 0.8 //2.8mm bulletDragTypeName = AnalyticEstimate } @@ -602,10 +642,10 @@ BULLET name = 40x311mmHEBullet DisplayName = 40mm Bofors caliber = 40 - bulletVelocity = 242 + bulletVelocity = 540 bulletMass = 0.3500 //HE Bullet Values - explosive = True + explosive = Standard incendiary = False tntMass = 0.25 fuzeType = Impact @@ -613,7 +653,7 @@ BULLET fadeColor = False startColor = 167, 218, 240, 0 subProjectileCount = 1 - apBulletMod = 0.8 + apBulletMod = 0.8 //13mm bulletDragTypeName = AnalyticEstimate } @@ -621,11 +661,11 @@ BULLET { name = 57mmBullet DisplayName = 57mm Shell - caliber = 70 + caliber = 57 bulletVelocity = 1035 bulletMass = 2.4 //HE Bullet Values - explosive = True + explosive = Standard incendiary = False tntMass = 1.2 fuzeType = Impact @@ -633,7 +673,7 @@ BULLET fadeColor = False startColor = 240, 190, 128, 0 subProjectileCount = 1 - apBulletMod = 0.8 + apBulletMod = 0.8 //73mm bulletDragTypeName = AnalyticEstimate } @@ -645,7 +685,7 @@ BULLET bulletVelocity = 620 bulletMass = 6.8 //HE Bullet Values - explosive = True + explosive = Standard incendiary = False tntMass = 5.44 fuzeType = Impact @@ -653,7 +693,7 @@ BULLET fadeColor = False startColor = 248, 247, 230, 0 subProjectileCount = 1 - apBulletMod = 0.8 + apBulletMod = 0.8 //48mm bulletDragTypeName = AnalyticEstimate } @@ -665,7 +705,7 @@ BULLET bulletVelocity = 915 bulletMass = 6.8 //HE Bullet Values - explosive = True + explosive = Standard incendiary = False tntMass = 5.44 fuzeType = Impact @@ -673,7 +713,7 @@ BULLET fadeColor = False startColor = 128, 128, 128, 0 subProjectileCount = 1 - apBulletMod = 0.8 + apBulletMod = 0.8 //88mm bulletDragTypeName = AnalyticEstimate } @@ -685,7 +725,7 @@ BULLET bulletVelocity = 5000 bulletMass = 1.25 //HE Bullet Values - explosive = False + explosive = None incendiary = False tntMass = 0 fuzeType = None @@ -693,7 +733,7 @@ BULLET fadeColor = False startColor = 122, 170, 240, 0 subProjectileCount = 1 - apBulletMod = 1.5 + apBulletMod = 1 //500mm bulletDragTypeName = AnalyticEstimate } @@ -705,7 +745,7 @@ BULLET bulletVelocity = 3000 bulletMass = 500 //HE Bullet Values - explosive = False + explosive = None incendiary = False nuclear = True EMP = True @@ -715,7 +755,7 @@ BULLET fadeColor = False startColor = 128, 128, 128, 0 subProjectileCount = 1 - apBulletMod = 1 + apBulletMod = 1 //N/A bulletDragTypeName = AnalyticEstimate } @@ -728,7 +768,7 @@ BULLET bulletVelocity = 4000 bulletMass = 0.125 //HE Bullet Values - explosive = False + explosive = None incendiary = True tntMass = 0 fuzeType = None @@ -736,7 +776,7 @@ BULLET fadeColor = False startColor = 0, 240, 100, 0 subProjectileCount = 1 - apBulletMod = 1 + apBulletMod = 0.1 //14mm bulletDragTypeName = AnalyticEstimate } @@ -747,9 +787,9 @@ BULLET DisplayName = 90mm Bullet caliber = 90 bulletVelocity = 850 - bulletMass = 19 + bulletMass = 14 //HE Bullet Values - explosive = False + explosive = None incendiary = False tntMass = 0 fuzeType = None @@ -757,7 +797,7 @@ BULLET fadeColor = False startColor = 128, 128, 128, 0 subProjectileCount = 1 - apBulletMod = 1 + apBulletMod = 1 //133mm bulletDragTypeName = AnalyticEstimate } @@ -768,9 +808,9 @@ BULLET DisplayName = 100mm Bullet caliber = 100 bulletVelocity = 1020 - bulletMass = 15 + bulletMass = 14.75 //HE Bullet Values - explosive = False + explosive = None incendiary = False tntMass = 0 fuzeType = None @@ -778,7 +818,7 @@ BULLET fadeColor = False startColor = 128, 128, 128, 0 subProjectileCount = 1 - apBulletMod = 1 + apBulletMod = 1 //165mm bulletDragTypeName = AnalyticEstimate } @@ -789,9 +829,9 @@ BULLET DisplayName = 105mm AP caliber = 105 bulletVelocity = 1020 - bulletMass = 19.6 + bulletMass = 19.5 //HE Bullet Values - explosive = False + explosive = None incendiary = False tntMass = 0 fuzeType = None @@ -799,7 +839,7 @@ BULLET fadeColor = False startColor = 128, 128, 128, 0 subProjectileCount = 1 - apBulletMod = 1 + apBulletMod = 1 //187mm bulletDragTypeName = AnalyticEstimate } @@ -811,7 +851,7 @@ BULLET bulletVelocity = 150 bulletMass = 25 //HE Bullet Values - explosive = True + explosive = Standard incendiary = False tntMass = 24.5 fuzeType = Impact @@ -819,7 +859,7 @@ BULLET fadeColor = False startColor = 128, 128, 128, 0 subProjectileCount = 1 - apBulletMod = 1 + apBulletMod = 1 //6mm bulletDragTypeName = AnalyticEstimate } @@ -831,7 +871,7 @@ BULLET bulletVelocity = 1070 bulletMass = 17.8 //HE Bullet Values - explosive = True + explosive = Standard incendiary = False tntMass = 35 fuzeType = Flak @@ -851,7 +891,7 @@ BULLET bulletVelocity = 900 bulletMass = 0.096 //HE Bullet Values - explosive = False + explosive = None incendiary = False tntMass = 0 fuzeType = None @@ -859,7 +899,7 @@ BULLET fadeColor = False startColor = 128, 128, 128, 0 subProjectileCount = 20 - apBulletMod = 1 + apBulletMod = 1 //35mm bulletDragTypeName = AnalyticEstimate } @@ -871,7 +911,7 @@ BULLET bulletVelocity = 1020 bulletMass = 19.6 //HE Bullet Values - explosive = True + explosive = Standard incendiary = False tntMass = 15.68 fuzeType = Impact @@ -879,7 +919,7 @@ BULLET fadeColor = False startColor = 128, 128, 128, 0 subProjectileCount = 1 - apBulletMod = 0.8 + apBulletMod = 0.8 //150mm bulletDragTypeName = AnalyticEstimate } @@ -892,7 +932,7 @@ BULLET bulletVelocity = 746 bulletMass = 29.4 //HE Bullet Values - explosive = True + explosive = Standard incendiary = False tntMass = 23.52 fuzeType = Impact @@ -900,7 +940,7 @@ BULLET fadeColor = False startColor = 128, 128, 128, 0 subProjectileCount = 1 - apBulletMod = 0.8 + apBulletMod = 0.8 //111mm bulletDragTypeName = AnalyticEstimate } @@ -910,10 +950,10 @@ BULLET name = 120mmBullet DisplayName = 120mm AP caliber = 120 - bulletVelocity = 850 - bulletMass = 12 + bulletVelocity = 1200 + bulletMass = 20 //HE Bullet Values - explosive = False + explosive = None incendiary = False tntMass = 0 fuzeType = None @@ -921,27 +961,47 @@ BULLET fadeColor = False startColor = 128, 128, 128, 0 subProjectileCount = 1 - apBulletMod = 1.6 + apBulletMod = 1.0 //216mm bulletDragTypeName = AnalyticEstimate } BULLET { name = 120mmBulletHE - DisplayName = 120mm HE + DisplayName = 120mm HEAT caliber = 120 - bulletVelocity = 800 - bulletMass = 19.6 + bulletVelocity = 1140 + bulletMass = 13.1 //HE Bullet Values - explosive = True + explosive = Shaped incendiary = False - tntMass = 15.68 + tntMass = 1.5 fuzeType = Impact projectileColor = 250, 130, 0, 128 fadeColor = False startColor = 128, 128, 128, 0 subProjectileCount = 1 - apBulletMod = 0.8 + apBulletMod = 0.5981 // 210 mm + bulletDragTypeName = AnalyticEstimate +} + +BULLET +{ + name = 120mmBulletHEAT + DisplayName = 120mm HEAT + caliber = 120 + bulletVelocity = 1140 + bulletMass = 13.1 + //HE Bullet Values + explosive = Shaped + incendiary = False + tntMass = 2.360 + fuzeType = Impact + projectileColor = 250, 130, 0, 128 + fadeColor = False + startColor = 128, 128, 128, 0 + subProjectileCount = 1 + apBulletMod = 0.9057 // 480 mm bulletDragTypeName = AnalyticEstimate } @@ -949,11 +1009,11 @@ BULLET { name = 120mmBulletSabot DisplayName = 120mm Sabot - caliber = 20 - bulletVelocity = 1750 - bulletMass = 9 + caliber = 27 + bulletVelocity = 1690 + bulletMass = 4.92 //HE Bullet Values - explosive = False + explosive = None incendiary = True //DU is pyrolic tntMass = 0 fuzeType = None @@ -961,7 +1021,7 @@ BULLET fadeColor = False startColor = 128, 128, 128, 0 subProjectileCount = 1 - apBulletMod = 2.5 + apBulletMod = 0.7838 // 629mm bulletDragTypeName = AnalyticEstimate } @@ -973,7 +1033,7 @@ BULLET bulletVelocity = 1200 bulletMass = 0.17 //HE Bullet Values - explosive = False + explosive = None incendiary = False tntMass = 0 fuzeType = None @@ -981,7 +1041,7 @@ BULLET fadeColor = False startColor = 128, 128, 128, 0 subProjectileCount = 40 - apBulletMod = 0.7 + apBulletMod = 0.7 //40mm bulletDragTypeName = AnalyticEstimate } @@ -993,7 +1053,7 @@ BULLET bulletVelocity = 685 bulletMass = 22.3 //HE Bullet Values - explosive = True + explosive = Standard incendiary = False tntMass = 17.84 fuzeType = Impact @@ -1001,7 +1061,7 @@ BULLET fadeColor = False startColor = 128, 128, 128, 0 subProjectileCount = 1 - apBulletMod = 0.8 + apBulletMod = 0.8 //77mm bulletDragTypeName = AnalyticEstimate } @@ -1014,7 +1074,7 @@ BULLET bulletVelocity = 915 bulletMass = 18.4 //HE Bullet Values - explosive = True + explosive = Standard incendiary = False tntMass = 8.5 fuzeType = Impact @@ -1022,7 +1082,7 @@ BULLET fadeColor = False startColor = 128, 128, 128, 0 subProjectileCount = 1 - apBulletMod = 0.9 + apBulletMod = 0.9 //124mm bulletDragTypeName = AnalyticEstimate } @@ -1032,10 +1092,10 @@ BULLET name = 125mmBulletSabot DisplayName = 125mm Sabot caliber = 25 - bulletVelocity = 2050 - bulletMass = 9.52 + bulletVelocity = 1660 + bulletMass = 5.12 //HE Bullet Values - explosive = False + explosive = None incendiary = True tntMass = 0 fuzeType = None @@ -1043,7 +1103,7 @@ BULLET fadeColor = False startColor = 128, 128, 128, 0 subProjectileCount = 1 - apBulletMod = 2.5 + apBulletMod = 0.6115 // 580 mm bulletDragTypeName = AnalyticEstimate } @@ -1051,12 +1111,12 @@ BULLET BULLET { name = 130Bullet - DisplayName = 120mm AP + DisplayName = 130mm AP caliber = 130 bulletVelocity = 725 bulletMass = 25 //HE Bullet Values - explosive = False + explosive = None incendiary = False tntMass = 0 fuzeType = None @@ -1064,7 +1124,7 @@ BULLET fadeColor = False startColor = 128, 128, 128, 0 subProjectileCount = 1 - apBulletMod = 1 + apBulletMod = 1 //108mm bulletDragTypeName = AnalyticEstimate } @@ -1077,7 +1137,7 @@ BULLET bulletVelocity = 814 bulletMass = 36.29 //HE Bullet Values - explosive = True + explosive = Standard incendiary = False tntMass = 29 fuzeType = Delay @@ -1085,7 +1145,7 @@ BULLET fadeColor = False startColor = 128, 128, 128, 0 subProjectileCount = 1 - apBulletMod = 0.8 + apBulletMod = 0.8 //127mm bulletDragTypeName = AnalyticEstimate } @@ -1098,7 +1158,7 @@ BULLET bulletVelocity = 563 bulletMass = 90.7 //HE Bullet Values - explosive = True + explosive = Standard incendiary = False tntMass = 72.56 fuzeType = Impact @@ -1106,7 +1166,7 @@ BULLET fadeColor = False startColor = 128, 128, 128, 0 subProjectileCount = 1 - apBulletMod = 0.8 + apBulletMod = 0.8 //105mm bulletDragTypeName = AnalyticEstimate } @@ -1119,7 +1179,7 @@ BULLET bulletVelocity = 607 bulletMass = 100 //HE Bullet Values - explosive = True + explosive = Standard incendiary = False tntMass = 80 fuzeType = Delay @@ -1127,7 +1187,7 @@ BULLET fadeColor = False startColor = 128, 128, 128, 0 subProjectileCount = 1 - apBulletMod = 0.8 + apBulletMod = 0.8 //102mm bulletDragTypeName = AnalyticEstimate } @@ -1140,7 +1200,7 @@ BULLET bulletVelocity = 629 bulletMass = 636 //HE Bullet Values - explosive = True + explosive = Standard incendiary = False tntMass = 508 fuzeType = Penetrating @@ -1148,7 +1208,7 @@ BULLET fadeColor = False startColor = 128, 128, 128, 0 subProjectileCount = 1 - apBulletMod = 1.7 + apBulletMod = 1.7 //439mm bulletDragTypeName = AnalyticEstimate -} +} \ No newline at end of file diff --git a/BDArmory/Distribution/GameData/BDArmory/BulletDefs/BD_Materials.cfg b/BDArmory/Distribution/GameData/BDArmory/BulletDefs/BD_Materials.cfg new file mode 100644 index 000000000..a95eb1863 --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/BulletDefs/BD_Materials.cfg @@ -0,0 +1,75 @@ +//////////////////////////////////////////////////////// +// Default Hull Material Config - Do Not Change +//////////////////////////////////////////////////////// + +MATERIAL +{ + name = def // internal reference name + localizedName = //Name displayed in-game + massMod = 1 //mass multiplier, 1 is default + costMod = 1 // cost multiplier, 1 is default + healthMod = 1 // HP multiplier, 1 is default + ignitionTemp = -1 // if positive, part is flammable; will catch fire above this temp + maxTemp = -1 // part's maxTemp. If negative, .cfg default + ImpactMod = 1 //crashTolerance multiplier, 1 is default +} + +//////////////////////////////////////////////////////// +// End Default Material Config +//////////////////////////////////////////////////////// +MATERIAL +{ + name = Wood + localizedName = #LOC_BDArmory_Wood + massMod = 0.33 + costMod = 0.5 + healthMod = 0.25 + ignitionTemp = 510 + maxTemp = 700 + ImpactMod = 0.5 +} +MATERIAL +{ + name = Aluminium //default part material + localizedName = #LOC_BDArmory_Aluminium + massMod = 1 + costMod = 1 + healthMod = 1 + ignitionTemp = -1 + maxTemp = -1 //934? + ImpactMod = 1 +} +MATERIAL +{ + name = Steel + localizedName = #LOC_BDArmory_Steel + massMod = 2 + costMod = 2 + healthMod = 1.75 + ignitionTemp = -1 + maxTemp = 1694 + ImpactMod = 2 +} +MATERIAL +{ + name = Titanium + localizedName = #LOC_BDArmory_Titanium + massMod = 1.5 + costMod = 1.5 + healthMod = 1.25 + ignitionTemp = -1 + maxTemp = 1941 + ImpactMod = 1.5 +} + +MATERIAL +{ + name = Composites + localizedName = #LOC_BDArmory_Composites + massMod = 0.6 + costMod = 3 + healthMod = 0.5 + ignitionTemp = 750 + maxTemp = 993 + ImpactMod = 0.75 +} diff --git a/BDArmory/Distribution/GameData/BDArmory/BulletDefs/BD_Rockets.cfg b/BDArmory/Distribution/GameData/BDArmory/BulletDefs/BD_Rockets.cfg index 09eb48ca1..348051d72 100644 --- a/BDArmory/Distribution/GameData/BDArmory/BulletDefs/BD_Rockets.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/BulletDefs/BD_Rockets.cfg @@ -5,17 +5,24 @@ ROCKET { name = def // do not change this! + DisplayName = def //display name for ammo in GUI rocketMass = 0.01 //in tons; 0.01 is 10 kg caliber = 70 //diameter of rocket + apMod = 1 //penetration modifier for the rocket thrust = 5 thrustTime = 1 //thrust duration, seconds shaped = False //shaped-charge warhead flak = False //proximity detonation EMP = False //EMP effect, can cause shutdown of target choker = False //localized atmospheric deprivation effect, kills airbreathing engines - gravitic = False //Modifies mass of target. use with weapon.cfg field 'massAdjustment' + gravitic = False //Modifies mass of target. impulse = False //Non-damaging concussive effect + massMod = 0 //mass(in tons) added on hit to target part + force = 0 //impulse amount imparted to target on hit explosive = True + nuclear = False //use nuclear detonation instead of tnt + beehive = False //rocket deploys submunitions + subMunitionType = //name of rocket or bullet to be released as a submunition if beehive is true, else leave blank incendiary = false tntMass = 1 //in kg subProjectileCount = 1 @@ -30,8 +37,10 @@ ROCKET ROCKET { name = Hydra70 + DisplayName = Hydra Rocket rocketMass = 0.012 caliber = 70 + apMod = 0.437 thrust = 6.2 thrustTime = 1.1 shaped = True @@ -51,6 +60,7 @@ ROCKET ROCKET { name = 8KOMS + Displayname = 8-KOMS Rocket rocketMass = 0.009 caliber = 70 thrust = 5.49 @@ -72,6 +82,7 @@ ROCKET ROCKET { name = FFAR70 + DisplayName = Folding-Fin Aerial Rocket rocketMass = 0.01 caliber = 70 thrust = 9 diff --git a/BDArmory/Distribution/GameData/BDArmory/ChangeLog.txt b/BDArmory/Distribution/GameData/BDArmory/ChangeLog.txt index 5c157cb7e..359ec937d 100644 --- a/BDArmory/Distribution/GameData/BDArmory/ChangeLog.txt +++ b/BDArmory/Distribution/GameData/BDArmory/ChangeLog.txt @@ -1,17 +1,1018 @@ -- General: +v1.6.3.0 +IMPROVEMENTS / FIXES +- General: + - Archive the defunct KSPedia files, reducing the size of the BDArmory zip file by around 24MB. +- UI: + - Add a button for resetting the scroll-zoom rate to 1 when "Debug Other" is enabled. + - Lock input to the numeric field when inputting values in custom log and semi-log sliders. + - Don't apply slider rounding when setting up log and semi-log sliders. +- Vessel Spawning: + - Adjust intra-team spacing to scale better with distance. + - Fix the inward/outward facing direction when spawning teams and use the same facing direction for all members of a team. +- Detectors: + - Now properly sends correct radar lock to SARH missiles when firing at multiple targets with a multi-lock radar. + - Properly lock SARH missiles when manually firing them. + - Equipping multiple lock-capable radars increases total radar locks the vessel can support. + - Attempting a radar lock will now check all radars capable of locking instead of just the first. +- AI: + - Smoothly vary the roll target during gain alt behaviour from the surface normal to vertical to avoid clipping wings into terrain when taking off on uneven terrain. + - Don't store/restore fields that are reference types. + - Make sure Time Override gets disabled when the AI gets destroyed if auto-tuning was enabled (should prevent it from being active unintentionally during competitions). + - Hide the 3-axis dynamic damping button when dynamic damping is disabled. + - Move the dynamic damping toggles to before the sliders to stop them from jumping around when toggled. + - Fix the PID field ordering when the PAW is first opened. + - Change the localisation for "controlSurfaceDeploymentTime" to "Vessel Reaction Time" to be more representative of its function. Also increase its range when unclamped. + - Fixed guard mode behavior with radarLOAL = false missiles when Max Missile Targets > Max Radar Locks on fire control radar. + - AI will now start leading a target when a gun is equipped 2s before entering max range. + - Adjust AI search pattern/drift when trying to find a recently lost target. + - Fix AI being able to see targets beyond set visual range. + - Fixes AI unlocking radar lock while in-flight missiles are using it. +- Weapons: + - Added (or corrected) penetration value for shaped charges to missile/gun/rocket part-menu infocards. + - Fixed armor penetration calculation for display in the part-menu infocards. + - Add an option when setting weapon groups to apply the group name to that weapon, symmetric weapons, all weapons of that type, all weapons of that class or applying to all guns/rockets/lasers. + - Missiles: + - New 'canRelock' field (default: true), sets if SARH radar-guided missiles will re-lock onto the active radar target if the original radar lock is lost. + - Further improvements to loft guidance, added a new field "terminalHomingType" which allows for selection of terminal homing logic. + - "aam"/"aamlead", "aampure", "pronav" and "augpronav" are currently supported. + - Both pronav options allow for user defined gains using "pronavGain" (recommended values of ~1-7). + - IMPORTANT: "LoftTermRange" has been renamed to "terminalHomingRange"! + - Fixed MultiMissileLauncher behavior with loft guidance, as well as pronav/augpronav. + - Added new missile field "terminalHoming", when true the missile uses its original homingType until "terminalHomingRange" is reached, then it will switch to "terminalHomingType". + - Fixed bug in heat seeker detonation behavior which caused some missiles to not detonate when they should. + - Some tweaks to multimissile decouple speed/direction and drop time. + - Fix DLZ NRE with multi-missile launchers. +- Competition: + - Fix Laser mutators. +- RWP: + - Add MAX_SAS_TORQUE setting to autoset max non-cockpit SAS torque across vessel at competiton start. + - Add Runway_Round S5R10 setting to autoset necessary AI settings/fields for Space combat. + +v1.6.2.0 +IMPROVEMENTS / FIXES +- General: + - Fix TweakScale config for Typhoon engine (again). + - Add Toggle/Enable/Disable pivot action groups to the Claw variants. + - Add an action group option to give unlimited pivot range to the Claw variants. + - Tweaks so that asteroids have the proper HP in flight mode. + - Optimisation of some vector operations. + - Adds variant texture for ground radars. + - Fix SIDAM texture variants for standard brown or greyscale. +- Armor: + - Make the adjustable armor 'clamped' and 'triangle type' options persistent and initialised correctly in the PAW. +- UI: + - Adjust formatting of some debug telemetry. + - Fix EMP Hellfire texture URL. + - Add an indent for missile and CM settings. +- Vessel Mover: + - Don't apply "Don't Avoid Collisions" while lowering vessels. +- Vessel Spawning: + - Re-register EVA Kerbals in spawned vessels as active, since KSP de-registers this for some reason. + - Add a default text for the custom template name field. + - Automatically add the current custom template to the list if it's not already there when clicking 'save' without requiring clicking 'new' first. (I.e., don't require 'new'→'save' for the initial template.) + - When using "Fill Seats" = "Cockpits or Combat Seat", include the first command seat if neither a cockpit nor combat seat are present. + - Fix an issue with the spawning routine hanging when using asteroids game modes. + - Fix the inward/outward spawn orientation for circular spawning. +- Detectors: + - Adds a new dish-only AWACS radar variant. +- Weapons: + - APS turrets now function when GuardMode is off to let them work on player-operated craft. + - Adds new 'dualModeAPS' field (default false) for APS turrets; if true, they can also be selected and used as standard weapons. + - ABL/GoalKeepers/Oerlikon are now configured as dual-mode anti-missile APS. + - Fixed aiming cursor remaining while turrets are slaved to a GPS target. + - Missiles: + - Fix AI not selecting laser missiles. + - SARH missiles now properly follow whatever the active locked radar target is. + - Added new loft guidance logic under the homingType "aamloft". + - Enables longer ranged engagements at high altitude by enabling higher terminal speeds. + - Added various tuning parameters accessible in the hangar (as well as in part configs) to tweak performance as desired. + - A more thorough explanation of the guidance law and the tuning parameters is available on the Github wiki https://github.com/BrettRyland/BDArmory/wiki/1.2.5-Missile-configuration#3-loft-guidance-missiles. + - Implemented missing pure pursuit guidance ("aampure"). + - Add new 'boosterFuelMass' and 'cruiseFuelMass' fields to missile config that allow missiles to have a fuel usage that can affect mass when the new field 'useFuel' is true. +- Competition: + - Add some checks for lacking an AI or an airborne AI being landed/splashed to the scoring for being considered as wrecked (MIA). + - Add competition messages about vessels colliding with asteroids. +- RWP: + - Use the current planetary body, not Kerbin for remote orchestration. + +v1.6.1.0 +IMPROVEMENTS / FIXES + - General: + - Fix TweakScale config for Typhoon engine. + - Reduce min size, scaling increment for procArmor panels. + - Internal refactor of vessel spawning/moving. + - EAS-2 External Combat Seat now has a unique model(credit: Eclipse) to help differentiate it from the stock EAS-1. +- UI: + - Maintain the camera angle and distance when switching vessels with the Vessel Switcher (page up/down). +- Vessel Mover: + - Abort vessel selection and reset state when closing/hiding the VM window. +- Vessel Spawning: + - Use the better Vessel Mover lowering routines to lower spawned craft to the terrain. 'Ease-in Speed' is now the same as 'Min Lower Speed' in VM. + - Fix the black screen caused by an invalid camera target during custom template spawning. + - Maintain the camera angle when switching to spawn locations. + - Also maintain the camera distance if bringing the current vessel with us when switching to spawn locations. +- AI: + - Lower the minimum extend abort time to 1s. + - Set the main throttle axis group for the VTOL AI and the wheel throttle axis group for ground AI. (pitch, roll, yaw axis groups are already being set.) +- Detectors: + - Adjust IRST display implementation. + - Adds new 'omniDetection' field to RadarWarningReciever (default true). If false, RWR will only detect Radar missiles immediately, some changes to missile detection and CM response. + - Adds 'VARIABLE_MISSILE_VISIBILITY (default False) BDA setting for setting visual detection range of missiles based on their boost/cruise/post-thrust state. + - Add 'irstRanging' field (default false) to IRSTs to let them determine range and work like 'heat radar'. +- CounterMeasures: + - Add optional 'cooldownInterval' field for jammers/thermoptic CMs to add a cooldown before they can be used again. + - Implements smoke CM triggers, so the AI will actually use these now. +- Weapons: + - Adds 'minSafeDistanceDistance' field (default 0) to ModuleWeapon, for setting default weapon min ranges; AI will no longer fire if within min range. ADJUST AT OWN RISK. + - Fix ground Vees from firing wildly into the dirt if they lose sight of engaged ground target. + - Adds LoS check to direct-fire weapons. + - Missiles: + - Heatseeking missiles no longer home in on engine plume of engines that have turned off their Afterburner. + - Fix targeting offset for CR missiles. + - Fix NRE with jettisoning Multimissile Launchers. + - Adds ModularMissile ground target weapon selection logic. + - Give unguided/GPS missiles without coords/Radar missiles sans Radar/Laser missiles without Targeting Pod a dumbfire reticle. + - AI can now use dumbfired missiles (Targeting Mode = None / Radar/Laser missile without the necessary sensor), if no better option available. + - Add new 'radarTimeout' field to missile configs to permit dumbfired inertial guidance LOAL radar missiles. + - AI can now attampt to use guided bombs against air targets if engage Air = true and nothing else available. + - Fixed turret manual fire behavior, guns should now no longer fire unless they are correctly aligned with the point of aim. + - Fixed explosive erroneous cone-of-effect calculations for standard and shaped charge warheads. Should now function correctly. +- Competition: + - Pinata mode now temporarily disables Camera AutoSwitch, switches camera to Pinata until it is destroyed. + - Update the tournament parser to compute non-negative scores per round for waypoint races. + +v1.6.0.2 +IMPROVEMENTS / FIXES +- UI: + - Add a toggle for aim assist mode (Target = reticle placed at the closest point of approach of the projectile to the target, Aimer = reticle placed at the aiming position). + - Add localisation for 'Multiple' ammo type. +- Missiles: + - Fix missiles being able to fire. + +v1.6.0.1 +IMPROVEMENTS / FIXES +- General: + - Restore compatibility with KSP 1.9.1. + - Cleaned up some lingering issues with repulsor functionality when used outside of the 'Enable Repulsors' gamemode. +- UI: + - Expand 'Edit Inputs' scrollview enough to show all the shortcuts. +- AI: + - Don't turn on peace mode when auto-tuning. +- Gamemodes: + - Add BD_PART_STRENGTH BattleDamage option to have part/joint strength weaken as parts take damage. +- Weapons: + - Fix clustermissiles being non-functional. + +v1.6.0.0 +IMPROVEMENTS / FIXES +- General: + - Add restore/wipe functionality to the controlled axes of the KAL. + - Toggling the "Restore KAL" toggle in the GamePlay section of the settings will restore or wipe the controlled axes from KALs on all loaded vessels. + - Vessels spawned via BDA+'s spawning routines will automatically have this applied. + - The KALs on all loaded vessels will also be restored/wiped on competition start. + - Update the defaults for a number of BDA+ settings. +- UI: + - Auto-switch UI focus to new input text fields in Vessel Mover and Custom Spawn Templates. + - Move the "Auto-Disable UI" toggle to the UI section of the settings. + - Fix the AIR-2 Genie manufacturer localization tag. +- AI: + - Axis groups: set the various axis groups when setting the flight control state, allowing the BDA+ AIs to use these axis groups. + - Pilot and VTOL AI set the main throttle, pitch, yaw and roll axis groups. + - Surface AI, additionally, sets the wheel steer axis group. + - Battledamage is automatically disabled when Autotuning; no more craft setting themselves on fire from fueltank fires. +- Detectors: + - IRST now properly returns data to heatseeking missiles. +- Countermeasures: + - rcsReductionFactor in ECMJammers can now be set > 1 for making radar reflectors and similar. +- Weapons: + - Gun recoil now properly takes propellant mass into account. + - Multiple fireanim support for single barrel weapons. + - Fix impulse for lasers. + - In light of the increased damage following the HE damage calc fix in 1.5.9.0, the Explosive Damage Multiplier default has changed from 0.65 to 0.55. Note: this needs to be reset by the user. + - FireAngle default reduced from 1.4 to 1.0. + - Gun accuracy refactored across the board. + - Browning accuracy changed to 0.32. + - Vulcan accuracy changed to 0.88. + - Vulcan Turret accuracy changed to 1.02. + - GAU-8 & GAU-22 accuracy changed to 0.6405. + - Goalkeeper accuracy changed to 0.675. + - Chaingun accuracy changed to 0.45. + - Sidam accuracy changed to 0.42. +- Missiles: + - Fix issue that had broken MultiMissileLauncher parts last update. + - Add ability to jettison Multi-Missile rails via 'permitJettison = true' in their .cfg. + - Add 'displayOrdinance' bool for MultiMissileLaunchers for missilepods/bomb dispensers or similar where ordinance does not need to be visible. + - Add 'OverrideDropSettings' bool for MultiMissileLaunchers for reloadable rails and similar to fix missile settings getting overwritten on launch. + - Fix issue with detonation distance and heat seekers targeting the engine exhaust plume. +- GameModes: + - Add 'RepulsorOverride' bool to ModuleSpaceFriction, to allow Repulsors to be used outside of the SpaceHacks Gamemode (for making repulsor wheels and similar). +- Competition: + - Add sorting and "scores" for waypoint runs in the Vessel Switcher for waypoints game mode. +- VesselMover: + - Add option to immediately place the vessel after spawning. + - Adjustments to movement in map mode. + - Safer placement-lower. + +v1.5.9.4 +IMPROVEMENTS / FIXES +- GameModes: + - Rewrite how repulsors work, using the default altitude as the target altitude and a decaying exponential balanced at the target altitude and with velocity damping. Include a slider for setting the strength of the repulsor effect, which affects the scale of the exponential. +- UI: + - Update some Immelmann Turn text that was missed. + - Favour craft close to going through waypoints to switch the camera to. +- Weapons: + - Correction to manual aiming when the Krakensbane frame velocity is non-zero. +- RWP: + - Season 5 Round 5: + - Adjust how drag is applied to kerbals in S5R5 to give a smoother response. + - Set the Immelmann angle to 0 for S5R5 since the craft designs don't pitch well. + - Add a custom message for the GM kill for S5R5. + +v1.5.9.3 +IMPROVEMENTS / FIXES +- Internal: + - Add a Semi-Logarithmic FloatRange slider UI, which gives ranges where the values are of the form: 0.8, 0.9, 1, 2, 3, ..., 8, 9, 10, 20, 30, ..., 80, 90, 100, 200. +- AI: + - Adjustments to terrain avoidance defaults and how the threat range is calculated for better default terrain avoidance. + - Add a slider for setting the angular size of the cone for performing an Immelmann turn, in which the craft will simply pitch up to turn to a target in that direction. + - Fix for auto-tuning if the user moves the number of samples slider to lower than the current sample number. +- UI: + - Add a case-insensitive filter to the vessel selection windows in Vessel Mover and Custom Spawn Templates. + - Add a custom GUILayout.TextField with a grey placeholder string for nicer text entry boxes. + - Set turret slider step sizes based on the config limits: 20—200 steps. + - Add a close button to the VM window and tweak the colour scheme of other close buttons. +- Mod Integration: + - Add reflection utils from CameraTools. + - Add initial support for the MouseAimFlight mod. + - Disables the prevention of firing weapons when the mouse is over windows when the Mouse Aim mode is active. +- VesselMover: + - Better failure handling when VesselMover fails to spawn a vessel. + - Adjust movement speeds to better match that of the original VM. + - Adjust orientation for changes in the local up direction. + - Add an option to automatically close the VM window on competition start. +- Competition: + - Add an option to generate a CSV file for the overall PVP scores in the PVP parser. + - Use natural sorting to get the most recent tournament in the parser. + - Add a killer GM time threshold for running waypoint competitions, default: 60s. Craft that don't pass a waypoint within this time are killed off. + - Adjust the camera selection to favour craft near their max speed during waypoint races. +- RWP: + - Season 5, Round 5: + - Only clamp, not set, the maxBank and maxSpeed to the limits. + - Add drag to kerbals when exceeding 605m/s, scaling with the amount of overspeeding, to bring them back into line with the limit. + +v1.5.9.2 +IMPROVEMENTS / FIXES +- AI: + - Fix incorrect terrain avoidance critical angle conversion (degrees vs radians). + - Add a slider for setting the control surface deployment time used for terrain avoidance and reduce the amount the control surface deployment time affects the terrain detection distance. + - Note: the default is now lower than it was before and lowering it further can significantly reduce the terrain avoidance distance for highly manoeuverable craft. + +v1.5.9.1 +IMPROVEMENTS / FIXES +- General: + - Adjust how PartsBlacklist.cfg is read/written to merge the defaults with any existing custom values. + - Fix known exceptions and NaNs. +- Weapons: + - Use the combination of DEBUG_LINES and DEBUG_WEAPONS for showing the targeting component debug lines instead of only having them in the debug build. + - Further tweaks to the aim assist targeting reticle to account for the floating origin. + - Missiles/Bombs now arm their warheads when they reach a safe distance from their launching craft, to prevent anti-missile systems fragging craft by detonating ordinance immediately after launch. +- UI: + - Add a toolbar button for the BDA Vessel Mover. + - Make window states persistent across scene changes. +- Competition: + - Remove drag from seated Kerbals in the Waypoint race gamemode. + - Repulsor craft now spawn horizonally instead of nose down. +- RWP: + - Enable various RWP S5R5 overrides to settings. + - Max speed fixed at 600m/s. + - Min altitude clamped to below 50m. + - Max altitude enabled and set to 100m. + - Max bank fixed at 40°. + - Post-stall AoA fixed at 0°. + - Independent throttle disabled. + - Global lift multiplier increased to 0.1 from 0.036. + +v1.5.9.0 +IMPROVEMENTS / FIXES +- General: + - Blacklist the PotatoRoid due to it not having a valid prefab (they get procedurally generated) and causing NREs. + - Add try-catch around setting the gear action group due to the FSM sometimes breaking it and breaking pilot initialisation. + - Remove HP rounding and lower HP limit for MM-patched parts. Apply health modifier after applying rounding and lower HP limit for non-MM-patched parts. + - Add Infinite Propellant and Infinite Electricity Gamemode toggles to BDA settings menu. + - Some minor debug message cleanup. + - Updated German localisation from EzBro. + - Add Vessel Mover functionality directly in BDA: + - Spawning: + - Choice of classic or custom vessel selection: + - Classic is the built-in vessel selection window (slow to load, but gives more details). + - Custom is faster and remembers the scroll position. + - Crew selection (optional): + - The chosen crew are added to the spawned vessel in the order selected, up to the capacity of the vessel. + - If the "Fill Seats" option in the Vessel Spawner window is set to "Minimal", then only the selected crew are used, otherwise the remaining crew slots are filled with random kerbals. + - Option to create or remove kerbals from the roster (excluding Jeb, Val, Bill and Bob). + - Spawning uses BDA's spawning routines: + - Corrects for badly sorted part trees in craft files. + - Mostly fixes staging sequences (some parts, such as parachutes, still don't stage properly). + - Aligns craft with terrain/scenery normals based on control point orientation and facility the craft was built in. + - Default spawn orientation matches that of the KSC runway or launchpad, depending on which facility the craft was built in. + - Moving: + - Movement and rotation speeds scale with altitude instead of at preset speeds. + - Movement and rotation have a ramping up period to allow for fine adjustments. + - Movement automatically maintains altitude AGL but will adjust to avoid buildings and other craft. + - Movement coordinates are relative to the local coordinate frame with forward aligning with the camera. In map mode, forward is aligned with north. + - Tab/Shift+Tab jumps to preset altitudes (50km, 10km, 1km, 100m, 10m / minimum safe), 'x' jumps to the minimum safe altitude, other keybinds are the same as for the VesselMover mod (see the help button). + - Vessels are lowered to the terrain and then eased in for up to 10s to allow them to settle. + - Vessels can be dropped. + - Option for setting the minimum lowering speed. + - Options for enabling brakes/SAS when lowered. + - Option for placing vessels on terrain below water (also affects terrain following while moving). + - Option for placement-lowering (jump to safe height, then lower the rest of the way). + - Option for not worrying about colliding with stuff (for getting really close to things for screenshots). + - Movement indicator scales with the vessel's radius (+2m). + - Movement and spawning don't require the active vessel to be landed. + - Includes a button to recover the active vessel (i.e., remove it) from flight. + - Doesn't conflict with the VesselMover mod, but actively using both at the same time may cause weird interactions. + - Limitations: + - Sometimes, when moving beyond the limits of PRE, KSP can break due to automatic vessel switching. + - If the mouse is over a window when moving, the camera often lags behind (this is likely due to krakensbane and may be fixable). + - The camera may act weirdly if CameraTools is enabled while moving a vessel (also likely due to krakensbane). +- UI: + - Add a keybind for targeting the next GPS target. + - Add labels to the vessel switcher score entries for rockets, missiles, rams and tag. + - Parts with zero lift now appear as grey in the lift visualizer. + - Fix issues with wing stacking calculation. Calculation now returns ratio of non-separated vertically stacked lift area to total lift area (capped at 100%). Wings spaced sufficiently apart will not contribute to the stacking value. + - Add option "Camera Switch: Incl. Missiles" to switch to and follow a missile fired by the active vessel until it explodes or misses (requested from FJRT, requires CameraTools v1.27.0 for optimal behaviour). + - Add whitelist option for guns/rockets/lasers to report hits in the competition marquee. + - Fix manual aim assist UI placement for fixed weapons. +- AI / WM: + - AI will start evading missiles once the missile engine activates instead of waiting for 1 second after launch. + - When orbiting, adjust altitude for terrain variation when below 1000m and account for terrain in the way when below 500m. + - Remove the 'premature dive fix', which causes weird flight behaviour when flying inverted. + - Terrain avoidance improvements: + - When avoiding terrain and roll is inverted by more than the critical angle (default 120°), reverse the roll target so that terrain avoidance is done while fully inverted and enable Aiming steer mode for more yaw control. + - Remove the terrain avoidance cool-down period for faster recovery after avoiding terrain. + - Restrict the avoidance radius (default is twice the "radius" of the vessel) to the min altitude to allow cruising at min altitude despite without triggering terrain avoidance. + - Include all detected colliders in the terrain avoidance path and use a weighted average of normals to determine the appropriate terrain avoidance direction to fly (this helps with avoiding buildings). + - Remove the secondary raycast for upcoming terrain as it doesn't seem necessary and is doubtful it was helping. + - Adjust the max deflection for avoiding terrain to be based on the maxAoA clamped to within 45° and 70°. +- Armor / Materials: + - Steel Hull Material melting point increased. + - Fix issue with armor panels potentially spalling away more than the entire volume of the plate and getting instantly deleted. +- Countermeasures: + - Add selection priority to countermeasures to allow staggering of dropping countermeasures — higher priority CM dispensers trigger first. +- GameModes: + - Add ability to disable cornering multiplier when using space hacks by setting it to 0. + - Space Friction no longer applied to root part at the position of the CoM; large craft whould have an easier time turning now. +- Weapons: + - Weapon burst length/fire angle overrides now respect symmetry. + - Fix heatseeker missiles fired from relaodable rails occasionally losing lock on firing. + - Fix NRE spam when trying to fire Modular Missiles. + - Fix NRE when firing Modular Missiles and Missile Icons are enabled. + - Modular Missiles will use RCS when fired in space. + - Heatseekers now target the hottest part on a craft, not the CoM. + - Fix HE beehive ammo submunitions not exploding on hit. + - Re-add HE ammo option to Oerlikon Millennium turret. + - Fix some issues with Explosion damage calcs, HE damage is now consistant regardless of hit location. + - Fix ammo count in WM GUI for reloadable missiles. + - AI will now properly respect per-missile engage range when selecting and firing missiles of the same type. +- Competition: + - Add toggle for setting maxAltitude and GM Kill limits to ASL or AGL. + - Add Kill timer for GM Altitude to give violators a chance to get back to safe altitude instead of immediate kill. + - Add customisable fields for the intra-team separation conditions when starting competitions (default: 800m + 100m per member) (FJRT request). + - Add a per-round PVP score parser. + - Fix accuracy calcs when using beehive ammo. + - Fix vessel spawn altitude if RWP slider is set but not the RWP toggle. +- RWP: + - Fix issue causing the web API to not report scores. + - Tag heats run via the web API with competition, stage and heat numbers in the file names. + +v1.5.8.0 +IMPROVEMENTS / FIXES +- General: + - Fix Firebottle mass not resetting if a part switched from fueltank to structural. +- UI: + - Fix layout of vessel switcher window buttons and version label. + - Add debug button when DEBUG_OTHER is enabled for dumping part info. + - Add a 'Start competition automatically' button to the Vessel Spawner window for use with 'single spawn'. + - Updated German localisation from EzBro. + - Fix some AI GUI infotext. + - Clean up ordering and consistency in the AI GUI and in-flight WM windows. +- Competition: + - Tweaks to sequenced competition start-up to allow a single plane to perform the sequence despite not having enough teams for a competition. + - Add an option to save the image from plot_summary.py with a transparent background. +- AI / WM: + - Fix max collision avoidance strength in AI GUI. + - Fix NRE when adjusting autotuning fields in the SPH. + - AI will no longer dive into missiles approaching from directly below. + - Reset the auto-tune PID values to the best ones when lowering the learning rate. + - Add a keybind for arming/disarming the WM. +- Detectors + - Fix radars/IRSTs with ability to detect/lock targets with a signature of 0 not being able to detect/lock targets with a signature of 0. + - Fix a division by zero in the standoff jamming effect calculation when the radar signature is 0. +- Weapons: + - Fix bug in nuke impulse calculation and some NREs. + - Fix turrets on non-active vessels following the mouse unless Remote Firing enabled. + - Fix missiles fired from reloadable rails resetting missile settings on Launch/Spawn. + - Add new engineFailureRate (default is 0, max is 1) config file variable for missiles. This is the probability the missile engine will fail to start. It is evaluated once on missile launch. + - Add new guidanceFailureRate (default is 0, max is 1) config file variable for missiles. This is the probability per second the missile guidance will fail (0-1). It is evaluated every frame after launch. + - Add new fuseFailureRate (default is 0, max is 1) config file variable for BDExplosivePart modules. This is the probability the explosive fuse will fail. It is evaluated once when an explosive attempts to detonate. +- RWP: + - Reset the killer GM between competitions and don't enable the killer GM for S5R3 (the altitude limit is still active). + - Add option to have Pwing mass and HP scale with thickness. + +v1.5.7.0 +IMPROVEMENTS / FIXES +- General: + - Add option in BDA settings menu to set a max HP limit for parts. + - Armor Tool in the VAB/SPH renamed to BDA Craft Utilities Tool. + - Added experimental 0-100% indication of stacked lift surfaces to the BDA Craft Utilities Tool. +- AI / WM: + - Fix bug in collision avoidance that sometimes resulted in more collisions between teammates. + - Add action group to the WM for removing EVA kerbals' helmets (once the vessel becomes the active one). + - Re-centre between epochs during auto-tuning if the vessel drifts more than 15km from the start position. +- Detectors: + - Fix anti-aliasing settings affecting craft RCS. Craft will have consistent RCS regardless oF MSAA settings, note that craft may have slightly different RCS across different GPUs/graphics APIs. + - Adjust RCS scaling value to give 1 m^2 cross-section for a 1 m^2 cross-section sphere (a 25% increase). This setting was most likely previously set with MSAA enabled, so it was set incorrectly. +- Weapons: + - Fix a bug in bullet hit calculations within the initial bullet offset distance. + - Fix AP rounds not penetrating rear side of parts they penetrate. + - Beehive ammo deployment range now adjustable. + - Fix ExplosionFX spawnpoint for Shaped Charge bullets. + - Some fixes to nuke behavior in space. +- RWP + - Update NPC Swap code to work with current web API. + - Orbital deployment moved to Runway Project Round S5R3. + - Fix AIs chasing GM killed planes. + - Runway_Project toggle now sets all control surface actuation speeds to 30 deg/s. + - Re-implement PinataMode - Craft will spawn on the same team in a circle around a Pinata craft, FFA begins when Pinata dies. + - Use settings.cfg field PINATA_NAME to set name of pinata craft. Craft should be in same autospawn dir as the rest of the craft for the competition. + - Sequenced Competitions now work with Web orchestration. + +v1.5.6.2 +IMPROVEMENTS / FIXES +- General: + - Fix kerbal flags exploding from overheat when placed. + - Add some missing parts to HPFixes patch. +- UI: + - Fix IRSTs not responding to Radar Display hotkeys. + - Fix Radar Analysis Window Radar select UX. +- AI / WM: + - Add 'Random Part' subsystem targeting option. +- Weapons: + - Nukes now work in space again. + - Fix IR missiles not locking on when manually firing. + - Fix Lasers not hitting missiles. + - Tweaks Oerlikon Millennium turret to improve missile interception ability, reduce lag. + - Add frontAspectHeatModifier variable to missile definitions, explained further in sidewinder.cfg. Allows you to create IR missiles that behave more like early IR rear-aspect/tail-chase missiles. + - Remove the clamp on heat signature drop-off with range above 6 km. Prior to this change, any heat signatures more than 6 km away were capped at their 6 km value. +- RWP + - Add option for setting NPCs to a single team to Settings.cfg - REMOTE_ORC_NPCS_TEAM = [Teamname]. Leave blank for FFA NPCs. + +v1.5.6.1 +IMPROVEMENTS / FIXES +- Fix issue causing radars to return a cross-section of 0.0 for all craft. + +v1.5.6.0 +IMPROVEMENTS / FIXES +- General: + - Russian localisation thanks to user Akteon_. + - Add missing part localizations for radomes and combat seat. + - Fix EVA kerbals not always deploying their parachutes properly with Kerbal Safety enabled. + - Fix part costs for materials with cost modifiers equal/greater than 1. +- UI: + - Show the lift information even when the reset armour toggle is enabled. +- AI / WM: + - Allow selecting which fields to auto-tune in the AI GUI. (Fixed-P option removed from the PAW.) + - Show the best loss value in auto-tuning. + - Log the latest best auto-tuning values to the KSP.log even without debug AI enabled. + - AI will now try to recover from flat spins by idling engines and pointing nose downward. + - Allow damping values to go down to 0.1 instead of 1. + - Prevent the AI GUI from rounding slider values when not adjusted by hand (e.g., during auto-tuning). + - Craft with empty Multimissile launchers will now properly go into ramming mode (if enabled) if no other weapons. + - Make the burst fire override respect the firing interval. +- Weapons: + - Add a stagger option to barrage mode that fires barrages in bursts and adds variability to the reload times. Mostly only suitable for slow-firing ship guns. + - Hellfire/EMP Hellfire now better differentiated in Weapon Manager selection list. +- Competitions: + - Add a --no-header option to the tournament parsing script. + +v1.5.5.1 +IMPROVEMENTS / FIXES +- Fix NRE in trajectory sim. +- Fix active vessel not firing under AI control. +- Fix firing of missiles via hotkey not respecting ripple/single setting. + +v1.5.5.0 +IMPROVEMENTS / FIXES +- General: + - Add a --tsv option to the n-choose-k parsing script to output a TSV file instead of a CSV file. + - Tweaks to scroll-zoom prevention. + - Exclude weapons from the bounds calculations as they sometimes give weird values (e.g., lasers when firing). +- UI: + - Add an option in the UI settings for disabling scroll-zoom prevention when over BDA windows. + - Add version watermark to Competition UI and Vessel Switcher window. +- AI / WM: + - Fixes for Stationary surface vessel mode. + - No longer panics, even if airborne (allows for floating/free-fall turrets). + - Attitude control (using RCS and reaction wheels) is applied when an enemy is within the engagement range. + - Add 'Target Damage' Target Priority setting. + - Add keybindings for "Next Weapon", "Prev Weapon" and "Fire Missile" in the WM (keybindings for firing guns are on the weapons). +- Detectors + - Some optimizations to the ECMJammer code. + - Active jammers now appear on Radar Warning Receivers. +- Armor: + - Fix stack overflow in ToggleScaleClamp for symmetric parts. + - Further fixes for post-penetration bugs. +- Weapons: + - Remove the extra delay from dropping the final countermeasure in a sequence. + - Fix non-damaging lasers not scoring. + - Tightened up firing angle when using subsystem targeting; AI will now fire when subsystem part is within angle, not entire vessel. + - Fix missing NukeFX Flash. + - Fix the targeting reticle placement at very short range. + - Add a custom fire keybinding to weapons that can be used to temporarily enable and fire weapons with keys other than the main firing key. + - Click the toggle on the weapon PAW, then press the desired button. Left click to cancel, escape to clear. + - Add an action group for jettisoning rocket pods and toggle deployable rails. + - Fix the trajectory simulation and targeting reticle placement for rockets. +- Spawning: + - Allow a specific list of observers to be excluded from being removed during spawning and competitions. + - Allow spawning custom templates with only 1 team if not immediately starting a competition. + - Allow spawning non-valid craft via the custom spawn templates if not immediately starting a competition. +- RWP: + - Fixes for remote orchestration. + - Improvements to Spacehacks repulsor code; will no longer shred landed ships. + - Landing gear will now serve as repulsors when Repulsor mode is enabled instead of effect propagating through CoM, should result in more stable hovercraft. + +v1.5.4.1 +IMPROVEMENTS / FIXES +- General: + - Fix a crash-to-desktop due to firing missiles with action groups but without a WM. + - Add a 'recentlyFiring' property for CameraTools that checks all guns for having fired recently (instead of just the current one) and for having fired a missile recently. Make the camera vessel selection use this too. + - Internal fixes for hull/armour/HP setup logic. + - Fix Rocket and 25mm Ammo box HP. +- AI / WM: + - Fix not finding the AI in the SPH for the AI GUI. +- Weapons: + - Don't activate all engines on modular missiles when firing them, leave it up to the configured action groups. + - 0 damage beam lasers will no longer cause battledamage. + - Adjusts post-penetration calculations to prevent non-physical penetration/bullet mass adjustments. +- Spawning: + - Spawner will now spawn craft facing inwards if spawn distance is larger than competition distance. +- Armor/Hull: + - Armor Tool Hull Visualizer now displays all materials, not just Wood/Al/Steel. + - Aluminium armor plates now register their mass for Total Armor cost/mass in the Armor tool. + - Fix issue with parts using custom materials with greater than stock maxTemps still exploding at stock maxtemp. + - Add crashTolerance modifier to hull materials. + - Fix Hull Material modifiers not applying in Flight. + - Add user-customizable exclusion list (BDArmory/PluginData/PartMaterialsBlacklist.cfg) for IgnoredParts/parts that shouldn't have material options. + - Fires from incendiary rounds igniting flammable parts now generates heat if BD FIRE_HEATDMG enabled. +- RWP: + - Add Min Altitude Increases on Death game-mode. + +v1.5.4.0 +IMPROVEMENTS / FIXES +- General + - Fix Structural Pwing causing vessels to explode in flight. + - Fix Structural Pwing still shifting CoL sphere in SHP/VAB. + - Structural Pwing moved to structural tab. + - Rework the BDGUIComboBox. +- Armor / Hull: + - Setting material to wood and then back to Al/steel now properly resets maxTemp to default. + - Expanded Hull material functionality; users can now implement custom hull materials, similar to how the current armor system works. + - Adds new BD_Materials.cfg + - LEGACY_ARMOR toggle now properly sets armor panels back to having BDAc-era HP amounts. +- AI / WM: + - Allow kamikaze ramming of ground targets by overriding terrain avoidance and min alt behaviour when on final approach. + - Improved cornering by only reducing throttle to match speed when in aiming steer mode and increasing the max speed clamp for tight cornering when at higher AoA. +- Spawning: + - Custom Spawn Templates + - Set up your desired template by spawning vessels with VesselMover (or otherwise) and assigning teams. + - Create a new template from the current configuration or save over the current one. + - Edit the templates directly in BDArmory/PluginData/spawn_templates.cfg to adjust headings, etc. + - Load/delete existing templates. + - Assign vessels and specific kerbals to the template's slots, then spawn and start a competition without having to muck around with VesselMover or the "turning bug". +- Radar: + - Fix Radar GUI display range for datalinked radars. + - Fix IRST being unable to activate VRD without a radar enabled. +- UI: + - Fix the internal GUI rects being updated properly and add option to disable their visibility to the mouse. + - Scroll-zoom prevention while the mouse is over BDA+ windows. + - Fix various Armor Tool functionalities. + - Fix accuracy readout when Telemetry debug option enabled. +- Weapons: + - Add a penetration depth stat to weapons' partmenu infocards. + - Adjustments to post-penetration for high-velocity projectiles near L/D = 1. + - Fix Barrage rate of Fire for multi-barrel weapons with a singe fire animation. + - Fix Gravity gun (and other 0 damage lasers) annhilating their target/crashing the game. + - Weapons that only use ECPerShot (requestResourceAmount = 0) now properly fire again. + - Adds new field to bullet definitions [subProjectileDispersion] to allow setting custom dispersion cone for shotgun/beehive ammunition. + - Beehive ammo explosive submunitions are now properly explosive. + - Fix weapons nulling current target and throwing off lead offset calcs on vessels with long TargetScanIntervals. + - Missiles: + - Missiles/bombs in a salvo no longer fratricide each other when the first one detonates. + - Add launch offset param to MultiMissileLauncher. + - Fix MMLs unable to fire if using salvos smaller than total launcher count. + - MML salvoes now add one missile away per target fired on, not just primary target. + - MML missiles no longer labeled as [vesselname] debris when using Missile icons and Vessel Label UI Icon options. + - Fix mass for MultiMissilelauncher rails. + +v1.5.3.1 +IMPROVEMENTS / FIXES +- Spawning: + - Only spawn SpawnProbe for dead current vessel if spawning is aborted. +- AI / WM: + - Fix AoA limits above 90° not behaving as expected by only limiting the AoA if the limit is below 90°. + - Add an Afterburner Override Threshold slider (force AB active below this speed threshold if at full throttle). + - Add a post-stall AoA threshold for switching flight-mode when beyond this threshold. +- Weapons: + - Fix weapons with startup/shutdown animations getting stuck, preventing them from firing. + +v1.5.3.0 +IMPROVEMENTS / FIXES +- General: + - Use an alternative method of calculating vessel bounds to get the vessel radius. — Fixes some weird results with parasite fighters from KSP's internal function for this. + - Missile and countermeasure settings are now accessible in the Gameplay Settings section of the BDA Settings menu. + - Animate animations in the physics update so that they work with time-scaling. + - Localisation corrections/updates. Also in German. + - Lots of optimisations. + - Add Notes.md for devs with notes on optimisation and the current branches. + - Add caching for all AudioClips to reduce GC. + - Optimised access to Krakensbane and FloatingOrigin adjustments. + - Fix proc structural panel, now properly no longer has lift. + - HP log scaling now on by default when RUNWAY_PROJECT is enabled. +- UI: + - Updated German localization by EzBro. +- Competition: + - Add option to automatically disable the HUD on tournament start. + - Fixes nukes causing hit craft to be reported as 'Crashed and Burned' in competitions. +- Spawning: + - Fix spawning breakage due to PRE not being enabled and enable PRE checks and warnings during spawning. + - Recover KSP's camera after spawning failure. +- Damage: + - Fix the bug preventing explosive damage applying to buildings. + - Rework how damage is applied to buildings and how they regenerate. + - Add a building damage multiplier. + - Fix reverse raycasts that were using the wrong rays. + - Fix detonate direction in BDExplosive part, missiles/rockets with ContinuousRod/ShapedCharge warheads should now have properly oriented AoEs. + - EMP warheads can now be set to be hard/soft EMP in their configs. +- AI / WM: + - Add a "Debug Extending" button when AI debugging is enabled that prints extending debug info when clicked if the active vessel is extending. + - Fix GPS missiles not obeying max missile per target setting when the target is traveling at high speeds. + - Add an Extend Abort Time slider to the AI, where the AI will abort extending if it fails to gain distance for this amount of time. (Note: this isn't a pure timer; while not gaining distance the timer increases, but will otherwise reduce to 0 at half the rate.) + - Add a 5s cooldown on extending if the Extend Abort Time is triggered (overriden for various extend requests, e.g., dropping bombs/nukes). + - Dynamically update the extend distance for launching a missile while extending. + - Limit the DLZ calculation for the extend distance for firing missiles to being maxOffBoresight off-target - avoids extreme extending distances. + - Expand the WM's Self-Destruct AG to arm and detonate all explosive parts too. + - Adjust the smoothing for the AoA and G-load limiting from a moving window (with a ~0.6s delay) to double exponential smoothing (with a ~0.1 delay compensated for by a 0.1s ahead prediction). + - Note: this doesn't fix AoA/G-load limiting, it just improves the response time of its calculations. +- Detectors: + - Random position noise due to chaff no longer scales with RCS, is instead fixed to a random number between 16 and 256. +- ECM: + - ECM Jammer and Cloaking devices now can specify resource used when operating (default is EC). +- Weapons: + - Fixed bug where the target velocity and acceleration was not being reset once a target was no longer tracked which resulted in lead being erroneously compensated for while mouse aiming. + - Added ability to manually aim guns using GPS coordinates, primarily for artillery. This behavior is not a functionality in Guard Mode, however when a GPS target is manually selected, turrets will now aim to attempt to hit said target. + - Improved shaped charge warhead behavior, now armor penetration and beyond-armor effect is much more realistic. + - Warheads now approximately match real-world penetration performance, given the warhead size and caliber are accurate. Dual-purpose and other multi-function warheads will overperform as the equations assume all of the warhead is a shaped charge. Older and larger warheads may also overperform due to modelling being based on modern warheads. + - Fixed issue with missile and rocket warheads where direction of blasts were not along the directions of their warheads. + - Added "caliber" field to the BDExplosivePart module, it's an optional field for shaped charge projectiles which influences its effectiveness. + - apBulletMod now influences shaped charge projectile penetration, modifying this allows for the penetration of shaped charge shells to be tweaked. + - Added optional "apMod" field to the BDExplosivePart module, it's functions the same way as apBulletMod for bullets and its primary purpose is to allow users to create dual-purpose shaped charge warheads, where part of the tntMass of the warhead isn't going into the shaped charge effect, reducing penetration. + - Added optional "apMod" field to rocket configuration file. This allows tweaking of rocket penetration, functioning the same way as apBulletMod for bullets, affecting physical armor penetration as well as shaped charge armor penetration if the rocket is equipped with a shaped charge warhead. + - Armor protection against explosions is now influenced by angling, angling armor now makes it more difficult for the blast to punch through the armor plate. + - Added global setting that allows for tweaking of armor effectiveness against explosion blast penetration. The higher the setting, the harder to damage parts behind armor plates via blast damage. + - Tweaked several missiles, bullets and rockets to reflect the new shaped charge mechanics: + - RBS-15 and RBS-15AL have had their warheads decreased from 300 kg to 200 kg to reflect their real counterparts. Added caliber field, set at 500 mm. + - AGM-114R Hellfire missile has had its warhead decreased from 12 kg to 8 kg, with a 172 mm caliber to reflect performance estimates of earlier known models of the Hellfire. Warhead may not match -114R performance. + - AGM-65 Maverick missile is set to have a 305 mm caliber and an apMod field added with performance set to 950 mm of penetration. No good estimates of the warhead penetration were found so an approximate estimate based on a 27 kg charge was used. + - BGM-71 TOW missile has had its warhead decreased from 10 kg to 3.9 kg and has a 152 mm caliber to be around TOW/ITOW performance. + - Hydra-70 has had its penetration tweaked with apMod to match estimates of the M151 HEDP warhead penetration performance. + - AGM-86 now has caliber field, set to 620 mm. + - Adjusted 120 mm HEAT and AP rounds to have around the same amount of penetration, balanced for gameplay (210 mm / 216 mm). Realistic configs also exist under the names "120mmBulletHEAT" and "120mmBulletSabot", a commented out line in m1Abrams.cfg can be uncommented in order to use these. + - Offset beam-riding missile laser to the right and up from the targeting camera to mitigate issues of the camera locking on to its own missiles when on CoMLock mode, causing the targeting camera to drive missiles into the ground. Mostly affected Guard Mode behavior. + - Fixed missile behavior with Detonation Distance set to 0, missiles should no longer phase through armor. + - Reworks ModuleMissileRearm, now works, integrated with WM, etc. + - Missiles with a ModuleMissileRearm can now have reloads, be affected by Infinite Ammo. + - Adds MultiMissileLauncher module. + - Adds new Infinite Ordinance toggle, for missiles with ModuleMissileRearm/MultiMissileLauncher. + - Fixes NRE with APS missile interception. + - Inaccurate or multi-shot APS (CIWS rotary cannon, etc) no longer guaranteed to kill incoming projectiles with first shot. + - Fixes Max Turret Targets not targeting more than 2 vessels. + - Fix Gravity gun and other impulse/gravitic laser weapons dealing damage. + - Fix manual rocket turret aiming. +- Armor: + - Improved hypervelocity projectile post-penetration effects. Now Whipple shields should work properly, the more spacing between the armor plates the better the performance against hypervelocity rounds. + - Modified behavior of penetration formula below a projectile L/D ratio of 1 so penetration values are more consistent. + - Re-added temperature effects to armor protection levels, if the armor reaches its max safe temperature or goes above said temperature, performance is degraded. + - Added new Armor Aluminium material, based on Aluminium 7039 to provide a larger variety of armor materials to select from. + - Adds Armor Mass Mult slider to allow tweaking armor weight in-game. + - Armor Type "none" now disabled armor thickness slider, should fix PAW issue with 1.9.1 installs. + - Armor Tools now displays total wing srf. area, and wingloading kg/m2. + - Fix Armor tools mass/cost readout giving incorrect values when using global armor type. + +v1.5.2.1 +IMPROVEMENTS / FIXES +- Fix a variety of exceptions in pre-1.11.1 KSP due to missing API functions. +- Allow deploying to multiple KSP instances when compiling in Linux (add the extra KSP locations as lines in the ksp_dir.txt file). + +v1.5.2.0 +IMPROVEMENTS / FIXES +- General: + - Fix packaging issue in recent releases that contain incorrect capitalisation of certain files and folders. + - Adjust capitalization of "sounds" folders for parts to be consistent. + - Fix memory leaks caught by KSPCF. + - Make wheels susceptible to bullets and explosions too. + - Fix off-centered arrow on inline radome texture Git Issue #409 + - Re-fix lead issue on Apple Silicon. + - Adds optional adjustable Logarithmic HP clamp, replacing the Proc Part Max HP slider. + - Adds optional toggle to disable lift from colliderless PWing edges for better balance with stock wings/prevent pwing abuse. + - Apply the gapless particle emitter option to 'DecalGaplessParticleEmitter's too. + - Add Attachnode to the Ordinance bay to fix issues with Breaking Ground Robotics. + - Adds debugging messages to assist with making custom weapons. + - Adds MM patch to add liftless Proc Structural Panel if B9 Pwings installed. + - Reduce frequency (hopefully to none) of null FX object pool entries by catching unloading events too and triggering OnJustAboutToBeDestroyed before recovering vessels. + - Allow use of the combat seat as the root part. +- UI: + - Fix exception when resetting colours in Team Icons. + - Prevent the kill timer from showing in the Loaded Vessel Switcher window for surface vessels. + - Updated German localization by EzBro. + - Adds Accuracy readout to the Weapon Debugging telemetry. + - Setting the Armor Type to 'None' now disables the armor thickness slider; fixes PAW issue in KSP 1.9.1 installs. +- Competition: + - Fix exception in auto-resuming tournaments when SpawnProbe.craft isn't found. + - Fix incorrect Kill Steal attribution from 1v1 fights. + - Abort competition if a team leader still isn't ready to engage (airborne for pilot AI) by the time the start-competition-now timer runs out. + - Fixes AI/WM not attached to Root Part warning messages on competition start when using combat seats as the root part in RWP. + - Add option to start competitions despite failures occuring. For tournaments, this only applies after the third attempt fails. +- Spawning: + - Wait until after vessels are unpacked before reverting the spawn camera - fixes an exception with EVA kerbals and KSPCF. + - Fix freezing when vessel removal throws an exception. + - Account for wheels protruding into the ground during instant lowering of vessels. +- GameModes: + - Space Combat Tools: + - Better implement SpaceFriction initialization at round start. + - Fix AI sometimes limiting to idle speed instead of max speed in space. + - Add limits to prevent G/AoA limiter returning infinity values in space. + - Battle Damage: + - Add selfsealing options to Monoprop tanks. + - Tanks using B9PartSwitcher for fuel switching now get selfsealing options. +- AI: + - Fix incorrect inputField for off-target dynamic roll damping. + - Fix RippleIndex Errors from weapons overheating/reloading. + - Fix AI ignoring max missiles per target setting for GPS missiles + - Spaceborne pilotAIs will now dodge via RCS translation. + - Craft with guard mode enabled now automatically receive radar contacts via the datalink from friendlies if they have an onboard radar that can receive radar data (canReceiveRadarData = true in part). + - Corrections to aiming at orbital speeds and long distances (for deep space combat). + - Caveats: + - There is a discontinuity in the way KSP handles floating origin/Krakensbane adjustments at 100km above each world that can adversely affect trans-100km orbit aiming slightly (high bullet speeds mostly negates this). + - KSP won't load surface vessels when the current vessel is over ~90km (on Kerbin) for some unknown reason despite a sufficient PRE range, so orbital bombardment only works from LKO. + - The targeting reticle currently isn't being correctly placed when manually aiming at closeby targets (will be fixed later). +- Detectors: + - Fix IRST preventing Radar GUI close. + - Fix NRE in targeting camera when the camera parent transform is null. + - Re-work IR occlusion to look at engine and engine plume heat for non-prop engines, weight parts by mass and proximity to heat source for occlusion, and incorporate occlusion from the engine outside of a 50 deg angle of the engine exhaust. + - Correct spelling of "receive" (and variants) and adjust BDA parts that use "canRecieveRadarData" to use "canReceiveRadarData" instead ("canRecieveRadarData" is retained for compatibility, but other mods should make this adjustment!). +- ECM + - Radar missile position distortion due to chaff now relies on the ratio of ECM jammer strength to craft signature with RCS reduction to bias the position distortion from chaff to further behind the craft. Net effect is craft with jammers, and especially low RCS craft with jammers, will have an easier time evading radar missiles. + - Lockbreak from jammers now affects RCS with RCS reduction instead of base RCS. Craft can now mix RCS reduction parts and jammers to break locks without worrying about them conflicting. + - Incorporate stand-off jamming effects. Friendly craft with jammers now result in your craft being harder to detect and lock. Strength of this effect is determined by the lockBreakerStrength of the friendly jammers, relative distance of friendly jammers (friendly jammers closer to enemy will have a stronger effect), and if the jammers are close in field of view for the enemy (friendly jammers in same field of view will have a stronger effect). Debug readout is available by enabling the Detectors debug option. + - AI no longer will turn off jammers automatically if they were manually enabled prior to being turned on by the AI. +- Weapons: + - Fix FXLookAtConstraint modules on various turrets. + - Fix incorrect charge sound path for lasers. + - Make the ABL use proper looping audio and add a cool-down sound. + - Fix the ABL laser beam continuing to show while overheated when used manually. + - Fix for NRE when heatseeker is manually selected prior to activating guard mode. + - Fix Weapon manager PAW engagement options not affecting AI weapon selection in flight. + - Missiles and nukes will now properly play their explosion sound effects. + - AI controlled turrets will no longer return to rest position when overheated/reloading. + - Fix terminal heat guidance for missiles, including not being decoyed by flares. + - Improve consistency of anti-radiation distance target check. + - Add paramater gpsUpdates for GPS guided missiles. gpsUpdates >= 0 in the missile config will allow the missile to get position updates of the target from the craft that launched the missile (if it can still see the target) every gpsUpdates seconds. + - Corrections to tracer placement and alignment at orbital speeds (for deep space combat). + - Note: Enable "Vessel-Relative Bullet Checks" when at these speeds for correct collision checks between bullets and vessels. Disable it at lower speeds as it can be a CPU hog. + - Fix delay/penetrating fuze rounds sometimes not detonating. + - Penetrating rounds now register damage to all parts they hit, not just first. + - Fix 'Weapon requires EC' message spam if active vessel is not the one with the weapon in question. + - Weapons using ECPerShot now only drain ElectricCharge if the vessel has sufficient EC to fire the weapon. + - Use the explosion models and sounds given in MissileLauncher if none are specified in BDAExplosivePart. + - Adds Active Protection System implementation. + - Adds new fields: 'isAPS = T/F', 'APSType = [ballistic, missile,omni]'. + - Adds new Settings.cfg field 'APS_THRESHOLD = n' to set threshold bullet/rocket size for interception. +- Armor: + - Adjusted post-penetration behavior of high velocity rounds, rounds now erode instead of slowing down at high velocities. + - Multi-plate armor schemes now behave more realistically and will require more careful material selection and configuration. + - Whipple shields for hypervelocity rounds are now viable and desirable. This behavior is however still very crude and does not qualitative match realistic behavior. + - Adjusted stats for S-Glass and Kevlar materials to have realistic RHA equivalency. + - Re-implemented Tate-Alekseevksy equation for low-ductility materials. + - Added debug console output for armor penetration behavior. Now when Debug -> Armor is on, console will output armor penetration parameters. + +v1.5.1.2 +FIXES +- Fix adjustable armor unclamped scaling. +- Loosen distance checks for anti-radiation missiles fired at fast-moving targets (fixes instances when AI would not fire anti-radiation missiles at fast-moving targets). +- Fixes for some issues occuring on KSP 1.9. + +v1.5.1.1 +FIXES +- Fix AI not firing anti-radiation missiles at targets detected via RWR. +- Fix KeyNotFoundException error in weapon selection code. + +v1.5.1.0 +IMPROVEMENTS / FIXES +- General: + - Remove the "-pre" from the version string, fix some typos. +- AI: + - Allow a vessel that's lost parts after launch to still be auto-tuned. + - Don't reset the assigned fly-to position when arriving at an orbiting point so that planes actually orbit the targeted point. + - Resume various pilot commands if interrupted once the cause of the interruption is removed. + - Adjust the fly-to point's altitude closer to the default altitude when at longer ranges so planes with higher default altitudes will attack from above and vice-versa. + - Reduce the half-life of the velocity smoothing for the turn radius calculation to allow faster updates for terrain avoidance. + - AI can now fire antirad missiles at targets detected via RWR. + - Fix AI not firing heatseekers. + - Fix launch authorisation check throwing an NRE for modular missiles. + - Further fix to bombs ignoring max missiles/tge. +- Competition: + - Calculate the proper separation between teams for starting a competition - fixes the competitions not starting bug. + - Set the number of teams to 0 in one-at-a-time waypoint mode tournaments so spawning works correctly. + - Ignore modular missile engines when activating all engines during spawning. + - Fix the waypoint altitude slider to actually apply to waypoints when starting a waypoint run. +- Weapons: + - Add pro-nav gain parameter as tunable parameter for missile parts (pronavGain), set pro-nav gain to 3 by default. + - Anti-rad missiles now continue to target the last known GPS point instead of disabling guidance. + - Fix ShapedCharge warheads doing no damage. + - Activate modular missile engines directly instead of using staging (which sometimes doesn't work). + - Apply Floating Origin/Krakensbane corrections to missile guidances - fixes missiles detonating partway to target due to miss checks. +- Armor: + - Fix ProcArmor max scalar clamp not appearing. + - Add Yield and YoungModulus fields to armour for reflection - fixes armour definitions without these properties not loading properly. + - Tweak penetration calcs for ultra-low ductility armor materials. +- UI: + - Missile names now linked to launching craft teamcolor. + - Set the AI GUI window rect in Start, not Awake, to guarantee that BDArmorySettings values have been read - fixes the 0-height AI GUI window. + +v1.5.0.0 +FIXES +- Fix 'resizeSquare' texture not being loaded sometimes (due to a race condition?). +- Clean up log spam from ExplosionFX. +- Fix Ammo Selection Tool GUI when selecting a weapon using legacy ammo definitions. +- Fix ATG missiles not respecting maxMissilesPerTarget. +- Fixes Procwing HP and Armor calcs when using FAR. +- Fix ProcParts not updating HP/Armor volume. +- Adds missing FFAR rocket ammo to universal ammo boxes. +- Fixes TOW missile FX. +- Fixes Proc Armor bugs. +- Fixes broken hydraulics on Chaingun, Hydra Turret and Patriot Launcher models. +- Fixes custom armor volumes in .cfgs being ignored. +- Fix some EMP weapon Selection logic. +- EMP AMRAAM and EMP Hellfire missiles now have different names to differentiate them from vanilla variants. +- Fixes Armor explosion resistance. +- Armor panels no longer indestructible. +- VTOL AI no longer toggles off guard mode in competitions. +- Fixes Debris icons not appearing when Team Icons enabled. +- Fixes Chaingun/Rocketpod Rounds Per Minute slider values when reverting to VAB/SPH. +- Fixes ripplefire issue with weapons with different rates of fire in one weapongroup. +- Fix for issue where AI was not leading target when KSP was running on M1 Macs (Apple Silicon). +- Fixes JDAM inaccuracy issue. + +IMPROVEMENTS +- General: + - Rebranded as "BDArmory Plus" (BDA+) on SpaceDock/CKAN (from "BDArmory for Runway Project") and forum thread created. + - Major internal refactor to better organise code. + - Switch to a single DLL release. - Add missing HP rounding to some parts. + - Stop continuous single spawn when running a tournament. + - Optimise the PartExploderSystem to minimise creation of new vessels. + - CM Flares optimized, now have much reduced performance hit. + - Add a toggle to the UI settings section to disable flare smoke. + - Add a toggle to the UI settings section to disable gapless particle emitters (mostly used for engine trails). + - Debug labels now organized by type for easier debug tracking. + - Refactor the debug settings to be more specific and to have an on-screen only debug labels. + - Adds SelfSealingTanks/Firebottle options to Proc Part Tanks. + - Bullets/explosions/etc now ignore flags. + - Adds new CameraTools switching modes, right click the cameraTools button [A] on the VesselSwitcherGUI. + - [S] - Camera will autoswitch to vessel with highest score. + - [D] - Camera will switch to furthest vessel from dogfight centroid. + - Restore the precision of Vector2d persistent fields. + - Update the continuous spawning log parsing script. + - Add some extra layer masks (internal kerbals are 1<<16, wheels are 1<<26). + - Add a BodyUtils.GetTerrainAltitudeAtPos function. + - Adds optional setting to clamp proc part/proc wing max HP to a configurable maximum when Runway Project enabled. + - Adds UI Icon team color reset button. + - Adds missile names to missile warning icons when vessel names enabled. + - Add a config option for the maximum time scaling. + - Add a new logarithmic float range PAW slider type. + - AI: - Add a 5° deadzone around being on target for dynamic damping to avoid feed-back loop in PID. + - Add a check for requested extending already being satisfied. + - Add terrain awareness to the pilot AI's waypoint fly-to direction. + - Add a 'Waypoint Terrain Avoidance' slider to the pilot AI to control the range and strength of the waypoint terrain avoidance reaction. + - Fixes AI GUI slider increments to match AI PAW. + - Adds Dynamic Damping P/Y/R readouts to AI GUI. + - Fix AI takeoff behavior. + - Add Store/Restore option to save control surface configs in Pilot AI. + - Add extend angle to AI to clamp climb/dive angle when doing Air-to-air extending. + - Add Air/Ground target preference weighting to Target Priorities. + - Make the extend toggle apply to air-to-air missiles as well as air-to-air guns. + - Update the AI GUI help text for the current extending settings. + - Make the AI GUI vertically resizable. + - AI will now head to target's last known position if contact lost (out of sight range/angle/radar detection), with accuracy of predicted position decaying over time. + - AI will now enable radars when guard mode enabled. + - Add an altitude steer limiter to the pilot AI. + - Apply user-defined steer limiters to roll. + - Force VTOL AI to aim towards targets when trying to fire missiles (if it can't already fire the missile). + - Add a PID auto-tuning mode: + - The AI will use gradient descent to optimize the plane's ability to turn to a range of headings and stabilize in those directions. + - The loss being minimized is ∫f(x,θ)dθ over the range θ ∈ (30°,120°) (using the midpoint Riemann sum), where f(x,θ) is ∫(δp²·(α+t²)/θ² + γ·δr²·(α+t)/100/θ)dt for the current PID values (x) and heading change (θ), where δp is the pointing error, δr is the roll error, α is the fast response relevance and γ is the roll relevance (which is automatically adjusted over time to balance the contribution from the pointing and roll errors). + - Usage: Once the plane is airborne (and not in combat), enable auto-tuning and set the sliders to the desired values (the defaults are reasonable starting points and can be preset in the SPH; adjusting some of the sliders will restart the auto-tuning), then allow the auto-tuning to run until it stops automatically when the learning rate (LR) decreases to below 1e-3. The PID values will revert to those giving the lowest loss and these will be stored so they can be restored in the SPH. + - Recommendations: + 1. Set the auto-tuning altitude and speed to those expected to be used in combat. + 2. Use 5-10x time-scaling. + 3. Avoid mountainous terrain. + 4. Tune without dynamic damping first and use the result as the starting point for dynamic damping with all the damping values set to the tuned static damping value and the dynamic damping factors set to 1. + 5. Since the PID values are (currently) being optimized for flying to fixed points, the tuned I value may not be optimal for moving targets in combat and a slightly larger I may be desirable. + - GameModes: - Waypoints: - Add support for multiple waypoint gate models. - Add waypoint scaling from 50-1000m. - Altitude overrides for S4R10. - Remember the previously used HoSTag. + - Add right click to 'Run waypoints' to spawn with the vessel spawner coordinates instead of the hard-coded ones. + - Add multiple courses and a selection UI. + - Convert Waypoints from hardcoded courses to a config based implementation. + - Add support for unique Waypoint names. + - Add support for independently scaling (model and approach threshold) individual waypoints. + - Add support for independently setting individual waypoint altitudes. + - Waypoint Scale and Waypoint Altitude are now global overrides that will set all WPs to the specified value if not set to default; else will use values from the course data. + - Adds support for off-world waypoint courses both locally and API-run competitions. + - Add a slider for activating guard mode when passing a waypoint. + - Allow multiple laps for waypoints. + +- Weapons: + - Use min of missile drop time or 2 sec instead of fixed two second period for check target will be within missile maxOffBoresight at future time. + - Sidam Turret retextured (thanks Concodroid!). + - Allow turrets to fire outside of visual range if they have a radar lock. + - Issue 348; weapons will no longer manually fire if mouse cursor is over Weapon Manager GUI. + - APBulletMod now implemented for sabot rounds. + - Adjusted Sabot depth calculation, should no longer overpenetrate quite so much. + - Sabots now recognized as AP ammo in ammo labels. + - Weapons and explosions no longer cause armor damage/shrapnel/spalling damage in paintball mode. + - Add charge-up mechanic for weapons. + - Adds new .cfg fields: ChargeTime = n; ChargeEachShot = T/F; hasChargeAnimation = T/F; chargeAnimName = animName. + - Weapon debug lines now account for frame velocity and flight integrator delay. + - Rockets now support beehive/nuclear warhead options. + - EMP weapons fired at targets with AI/ WM off will not cause them to activate when target reboots. + - Bullets now support shaped charge HE fillers (for HEAT rounds and similar): + - Bullet.cfg 'explosive =' field no longer true/false, now select from 'Standard' or 'Shaped' in place of explosive = true, or 'None' in place of explosive = false. + - Missiles: + - Adds Missile multi-targeting. + - Adds new Max Missile Tgts setting in WM to set max targets to engage with missiles. + - Adds incoming missile alert. + - Shift some missile targeting code from OnUpdate to OnFixedUpdate. + - Prevent heat-seeking missiles from targeting debris. + - Remove restrictions on heat-seeking missiles attacking ground targets. + - Prioritize anti-rad missiles first in using missiles against ground targets with active radars. + - Adjust DLZ calculation to allow for better missile usage at close ranges, small angles to target. + - Possibly fixed the 'Radar Stuck' bug. + - Add option to display distances on the RWR logarithmically. + - AI will now break off and extend away from target when firing nukes if within projected blast radius. + - Continuous Rod warhead missiles now target a point a few meters above their target to better take advantage of planar AoE. + - Missiles no longer prematurely detonate if DetonateAtMinimumDistance is enabled. + - Fix HARMs ignoring max missiles on target. + - Add chaffEffectivity parameter to missile configs. See AIM-120 config for more details. + - Modular Missile ActiveRadarRange can now be set to 0. + - Deprecate allAspect parameter, missile modders should now use uncagedLock instead (old missiles will automatically have the uncagedLock parameter value set to the value of allAspect. + - Implemented Proportional Navigation and Augmented Proportional Navigation as missile guidance options for missile parts and modular missiles. For missile parts, set: + - homingType = pronav [for Pro-Nav]. + - homingType = augpronav [for Augmented Pro-Nav]. + +- Armor: + - Adds Oblique Triangle Proc Armor panel. + - Proc Armor nodes on triangular panels now orient themselves to current angle of panel edge. + - Proc Armor max scale can now be set in .cfg, unclamped in SPH/VAB. + - Armor penetration formula now uses the Tate/Alekseevskii formula. Penetration depths should be similar in most cases for modded ammos, but may need recalibration of their apBulletMod. + - Add Lift Visualization, Total Lift, and Wing Loading readouts (stock only, disabled when using FAR) to the BDA Armor Tool. + +- Detectors + - Adds IRST implementation for thermal detection alternative to radar. + -Adds thermal occlusion mechanic; heatsources hidden behind other parts will be harder to detect by IRST. + - Adds support for optic/thermal cloaking countermeasures. + - Adds new AN/AAQ-42 IRST Pod part. + - Radars must now be capable of detecting a target before it can be locked. + +- Spawning: + - Refactor spawning into more modular coroutines for use with the spawn and orchestration strategies. + - Add an 'instant' lowering mode for vessel easing after spawn (VESSEL_SPAWN_EASE_IN_SPEED = 0). + - Add a test for spawning a vessel into an active competition using the SingleVesselSpawning class (debug build only). + - Randomise team spawn order (similar to random FFA ordering). + - Switch the camera to a random plane after spawning instead of the last plane spawned. + - RWP: - Hall of Shame now supports multiple craft. - Add HoS support to Waypoint mode. + v1.4.18.4 IMPROVEMENTS - Add slider in the pilot AI for controlling the waypoint pre-roll time. @@ -54,6 +1055,8 @@ FIXES - Fix ammo belt exploit. IMPROVEMENTS - General: + - Major internal refactor for code organisation and maintainability. + - BDArmory is now a single DLL (the old BDArmory.Core.dll will be detected and removed). - Add a 'Generate Clean Save' toggle for auto-loading to the KSC without generating a clean save (tournaments and evolution still generate a clean save). - AI: - New AI part: AI Vertical Takeoff and Landing Pilot (for helicopters, VTOL jets, and airships). @@ -410,6 +1413,7 @@ IMPROVEMENTS - Add ability to add custom spawn sites to spawn menu. - Add Persistent UI button to Vessel Switcher to keep VS and Web Orchestration windows open when F2 UI toggled off. - Add an estimate of the current memory usage to the settings menu when Auto-Quit Memory Threshold is enabled. + - Allow a user-set max value to the Time Scaling factor. - Game Modes: - Add secret Disco Mode. - Zombie Mode option moved to cheat mode access. @@ -424,6 +1428,7 @@ IMPROVEMENTS - Only adjust the altitude for failed spawns for ground spawns. - Allow specifying both '-c' and tournament dir to parse the specified tournament dir as a collection (i.e., without the tournament folder structure). - Rebalance the score weights in the parsing script. + - Add a Gauntlet tournament style for teams tournaments (like N-choose-K, but between two groups of teams). - Kerbal Safety: - Reconfigure inventories when the kerbal inventory slider is adjusted. - AI: @@ -1473,14 +2478,14 @@ IMPROVEMENTS - HeatRay = false // beam lasers now AoE, add heat instead of damage - ElectroLaser = false // lasers now have EMP effect, use ECPerShot - BulletType = a; b; c // for ammo swap, add each ammo type the weapon can use separated with a semicolon - - AirDetonation, proximityDetonation depreciated, moved to bulletDef.cfg + - AirDetonation, proximityDetonation deprecated, moved to bulletDef.cfg - New Bullet_defs settings - subprojectileCount = n // number of projectiles per round, default 1 - fuzeType = None // choose from None, Timed, Proximity, or Flak - projectileColor = 255, 15, 0, 128//RGBA 0-255, final color of shot if fadeColor = True - fadeColor = False //fade color from startColor to projectileColor? - startColor = 255, 90, 0, 32 // initial shot color if fadeColor = True - - BlastPower, Heat, radius depreciated + - BlastPower, Heat, radius deprecated - New Rocket_defs settings - rocketMass = n //weight in tons (10kg rocket is 0.01, etc) - caliber = n // rocket diameter @@ -2716,7 +3721,7 @@ v0.9.6 - IVA gun audio low-pass filter frequency is now configurable in settings.cfg - Removed redundant filter effects on lower depth TGP camera - Tracer size updates in OnWillRenderObject instead of looping through all pooled bullets for each camera -- Stopped guard debug log entries unless DRAW_DEBUG_LABELS is enabled +- Stopped guard debug log entries unless DEBUG_LABELS is enabled - Updated laser damage (Yski) - Guard will use long-range turrets for distant targets if no missiles available (Yski) diff --git a/BDArmory/Distribution/GameData/BDArmory/Localization/BDArmory.cfg b/BDArmory/Distribution/GameData/BDArmory/Localization/BDArmory.cfg index 6b7634b7c..97edb5a92 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Localization/BDArmory.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Localization/BDArmory.cfg @@ -38,6 +38,51 @@ Localization #autoLOC_bda_1000033 = - Lock/Track: <<1>> m^2 @ <<2>> km #autoLOC_bda_1000034 = - Lock/Track: (none) #autoLOC_bda_1000035 = - Ground clutter factor: <<1>> + #autoLOC_bda_1000036 = Disable IRST + #autoLOC_bda_1000037 = Enable IRST + #autoLOC_bda_1000038 = - Detection: <<1>> ºC @ <<2>> km + } + de + { + #autoLOC_bda_1000000 = Radar deaktivieren + #autoLOC_bda_1000001 = Radar aktivieren + #autoLOC_bda_1000002 = Luft-Boden-Rakete + #autoLOC_bda_1000003 = Kampfflugzeug + #autoLOC_bda_1000004 = AWACS + #autoLOC_bda_1000005 = Rakete + #autoLOC_bda_1000006 = Erkennung + #autoLOC_bda_1000007 = Unbekannt + #autoLOC_bda_1000008 = Radar Typ: <<1>> + #autoLOC_bda_1000009 = Range: <<1>> Meter + #autoLOC_bda_1000010 = RWR Bedrohungsart: <<1>> + #autoLOC_bda_1000011 = Kann erfassen: <<1>> + #autoLOC_bda_1000012 = Verfolgen-während-Erfassen: <<1>> + #autoLOC_bda_1000013 = Kann Aufschalten: <<1>> + #autoLOC_bda_1000014 = Kann Daten empfangen: <<1>> + #autoLOC_bda_1000015 = Gleichzeitige Aufschaltungen: <<1>> + #autoLOC_bda_1000016 = Radar braucht Strom + #autoLOC_bda_1000017 = SONAR + #autoLOC_bda_1000018 = Nur für Datenverbindung + #autoLOC_bda_1000019 = omnidirektional + #autoLOC_bda_1000020 = Peilrichtung + #autoLOC_bda_1000021 = Strom/Sec: <<1>> + #autoLOC_bda_1000022 = Sichtfeld: <<1>>° + #autoLOC_bda_1000023 = RWR Bedrohungsart: <<1>> + #autoLOC_bda_1000024 = Capabilities: + #autoLOC_bda_1000025 = - Erfassen: <<1>> + #autoLOC_bda_1000026 = - Verfolgen-während-Erfassen: <<1>> + #autoLOC_bda_1000027 = - Aufschalten: <<1>> + #autoLOC_bda_1000028 = - Maximale Aufschaltungen: <<1>> + #autoLOC_bda_1000029 = - Empfange Daten: <<1>> + #autoLOC_bda_1000030 = Leistung: + #autoLOC_bda_1000031 = - Sichtfeld: <<1>> m^2 @ <<2>> km + #autoLOC_bda_1000032 = - Erkennung: (none) + #autoLOC_bda_1000033 = - Aufschaltung/Verfolgung: <<1>> m^2 @ <<2>> km + #autoLOC_bda_1000034 = - Aufschaltung/Verfolgung: (none) + #autoLOC_bda_1000035 = - Bodenstörfaktor: <<1>> + #autoLOC_bda_1000036 = IRST deaktivieren + #autoLOC_bda_1000037 = IRST aktivieren + #autoLOC_bda_1000038 = - Erkennung: <<1>> ºC @ <<2>> km } es-es { diff --git a/BDArmory/Distribution/GameData/BDArmory/Localization/Localization.md b/BDArmory/Distribution/GameData/BDArmory/Localization/Localization.md new file mode 100644 index 000000000..f75b925d8 --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Localization/Localization.md @@ -0,0 +1,24 @@ +--BDA-RWP Localization-- + +If you want to help translate BDA, it would be a great help. + +--How to translate-- + +Localization files are easy to make. Create a copy of the BDarmory/Localization/en_us.cfg and BDArmory/Localization/UI/en-us.cfg files and rename it according to the language being translated: +* "es-es.cfg" for Spanish +* "es-mx.cfg" for Mexican Spanish +* "pt.cfg" for Portugese +* "fr.cfg" for French +* "de-de.cfg" for German +* "it.cfg" for Italian +* "ja.cfg" for Japanese +* "ru.cfg" for Russian +* "zh-cn.cfg" for Simplified Chinese + +Note: filename is not too important, the important part is the language identifier inside the `Localization` block, e.g., +``` +Localization +{ + de-de + { +``` diff --git a/BDArmory/Distribution/GameData/BDArmory/Localization/UI/de-de.cfg b/BDArmory/Distribution/GameData/BDArmory/Localization/UI/de-de.cfg new file mode 100644 index 000000000..720493197 --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Localization/UI/de-de.cfg @@ -0,0 +1,1135 @@ +Localization +{ + de-de + { + #LOC_BDArmory_Generic_OK = OK + #LOC_BDArmory_Generic_Cancel = Abbrechen + #LOC_BDArmory_Generic_New = Neu + #LOC_BDArmory_Generic_On = Ein + #LOC_BDArmory_Generic_Off = Aus + #LOC_BDArmory_Generic_Hide = Verbergen + #LOC_BDArmory_Generic_Show = Anzeigen + #LOC_BDArmory_Generic_Load = Laden + #LOC_BDArmory_Generic_Save = Speichern + #LOC_BDArmory_Generic_Help = Hilfe + #LOC_BDArmory_Generic_Select = Auswählen + #LOC_BDArmory_VesselStatus_Landed = (Gelandet) + #LOC_BDArmory_VesselStatus_Splashed = (Im Wasser) + #LOC_BDArmory_VesselStatus_Underwater = (Unter Wasser) + + #LOC_BDArmory_WMWindow_title = BDA Waffen-Kontrollsystem + #LOC_BDArmory_WMWindow_GuardModebtn = Wächtermodus + #LOC_BDArmory_WMWindow_ArmedText = Auslöser ist\u0020 + #LOC_BDArmory_WMWindow_ArmedText_ARMED = AKTIVIERT. + #LOC_BDArmory_WMWindow_ArmedText_DisArmed = DEAKTIVIERT. + #LOC_BDArmory_WMWindow_TeamText = Geschwader + #LOC_BDArmory_WMWindow_selectionText = Waffe: <<1>> + #LOC_BDArmory_WMWindow_barrageStagger = Gestaffelt + #LOC_BDArmory_WMWindow_rippleText1 = Sperrfeuer: <<1>> s/min + #LOC_BDArmory_WMWindow_rippleText2 = Salve + #LOC_BDArmory_WMWindow_rippleText3 = Feuerrate: <<1>> s/min + #LOC_BDArmory_WMWindow_rippleText4 = Feuerrate: AUS + #LOC_BDArmory_WMWindow_ListWeapons = Waffen + #LOC_BDArmory_WMWindow_GuardMenu = Wächter-Menü + #LOC_BDArmory_WMWindow_ModulesToggle = Module + #LOC_BDArmory_WMWindow_NoneWeapon = Kein(e) + #LOC_BDArmory_WMWindow_NoneWeapon = Wächtermodus <<1>> + #LOC_BDArmory_WMWindow_FiringInterval = Kadenz + #LOC_BDArmory_WMWindow_BurstLength = Feuerstoß-Dauer + #LOC_BDArmory_WMWindow_FiringTolerance = Winkel-Toleranz + #LOC_BDArmory_FiringPriority = Auswahlpriorität + #LOC_BDArmory_WMWindow_FieldofView = Gesichtsfeld + #LOC_BDArmory_WMWindow_VisualRange = Sichtbereich + #LOC_BDArmory_WMWindow_GunsRange = Feuerbereich (Weite) + #LOC_BDArmory_WMWindow_MultiTargetNum = Max. gleichz. Ziele (Kanonen) + #LOC_BDArmory_WMWindow_MultiMissileNum = Max. gleichz. Ziele (Raketen) + + #LOC_BDArmory_WMWindow_MissilesTgt = Raketen pro Ziel + #LOC_BDArmory_WMWindow_TargetType = Erweiterte Zieleinstellungen: + #LOC_BDArmory_WMWindow_TargetType_Missiles = Raketen + #LOC_BDArmory_WMWindow_TargetType_All = Alle Ziele + #LOC_BDArmory_WMWindow_RadarWarning = Radar-Warnung + #LOC_BDArmory_WMWindow_GPSCoordinator = GPS Koordinator + #LOC_BDArmory_WMWindow_WingCommand = Flügelmann + #LOC_BDArmory_WMWindow_NoWeaponManager = Kein Waffen-Kontrollsystem gefunden! + #LOC_BDArmory_WMWindow_GPSTarget = GPS Ziel + #LOC_BDArmory_WMWindow_NoTarget = Kein Ziel + + //WM inflolink + #LOC_BDArmory_WMWindow_Weapons_Desc = In diesem Menü werden alle an diesem Gefährt verfügbaren Waffen und Waffengruppen angezeigt. Waffen werden durch Klick auf den Namen der Waffen(gruppe) ausgewählt, so dass sie manuell abgefeuert werden können. Im Fall von Raketen, die eine Aufschaltung benötigen, muss zuvor der Auslöser aktiviert werden: Klick auf den 'Auslöser ist DEAKTIVIERT' Schalter. + + #LOC_BDArmory_WMWindow_Ripple_Salvo_Desc = Gelenkte Raketen haben eine einstellbare Feuerrate (Schuss pro Minute, s/min). Diese bestimmt, mit welcher Rate die Raketen abgefeuert werden, wenn der Auslöser gedrückt gehalten wird. Kanonen, ungelenkte Raketen, und Laserwaffen, die eine Feuerrate von unter 1500s/min haben, haben stattdessen einen Sperrfeuer/Salve Schalter. Wenn mehrere Waffen des gleichen Typs in der ausgewählten Gruppe sind, feuern diese bei der Einstellung 'Sperrfeuer' sequenziell, bei Salve gleichzeitig. + + #LOC_BDArmory_WMWindow_GuardTab_Desc = Wächter-Modus: Diese Einstellungen bestimmen, wie und wann das Waffen-Kontrollsystem die Waffen benutzen wird. + #LOC_BDArmory_WMWindow_FiringInterval_Desc = Feuer-Intervall - Bestimmt, wie häufig (alle wie viele Sekunden) das Waffen-Kontrollsystem nach potenziellen Zielen sucht. + #LOC_BDArmory_WMWindow_BurstLength_desc = Feuerstoß-Dauer - Bestimmt die Dauer des Feuerstoßes, wenn ein Ziel gefunden wird. Für Dauerfeuer: Feuerstoß-Dauer = Feuer-Intervall. Für kürzere Feuerstöße: Feuerstoß-Dauer < Feuer-Intervall. Wenn die Feuerstoß-Dauer auf Null gesetzt wird, werden Feuerstöße einer Länge von (0.5x Feuer-Intervall) abgegeben. + #LOC_BDArmory_WMWindow_FiringTolerance_desc = Winkel-Toleranz - Stellt ein, innerhalb welchen Winkelbereichs (Schussrichtung relativ zum Ziel) das Waffen-Kontrollsystem feuert. Ein Wert von 1 bedeutet, dass auf ein Ziel geschossen wird, wenn es sich innerhalb eines Konus befindet, dessen Weite dem Radius des Ziels PLUS dem durchschnittlichem Fehlwinkel der Waffe entspricht. Präzisere Waffen haben effektiv einen kleineren Ziel-Konus. Wenn der Wert vergrößert wird, vergrößert sich der Winkelbereich, innerhalb dessen auf ein Ziel geschossen werden wird. + #LOC_BDArmory_WMWindow_FieldofView_desc = Gesichtsfeld - Bestimmt das Gesichtsfeld des Waffen-Kontrollsystems. Ein Winkel von 360 Grad bedeutet, dass das Kontrollsystem alle Ziele in alle Richtungen erkennen kann. Mit einem kleineren Winkel kann ein Konus definiert werden, außerhalb dessen Ziele nicht erkannt werden. + #LOC_BDArmory_WMWindow_VisualRange_desc = Sichtbereich - Bestimmt, wie weit das Waffen-Kontrollsystem sehen kann. Nur Ziele, die näher sind als dieser Wert, werden erkannt und angesteuert. Ziele jenseits des Sichtbereichs können durch Radar erkannt werden. + #LOC_BDArmory_WMWindow_GunsRange_desc = Feuerbereich (Weite) - Bestimmt die maximale Reichweite gleichzeitig für alle Waffen. Wird automatisch auf die maximale Reichweite der Kanone gesetzt, die die größte Reichweite hat. Das Waffen-Kontrollsystem wird nicht auf Ziele schießen, die sich jenseits dieser Entfernung befinden. + #LOC_BDArmory_WMWindow_MultiTargetNum_desc = Maximale gleichzeitige Ziele - Zahl der gleichzeitig zu beschießenden Ziele, wenn mehrere bewegliche Geschütztürme vorhanden sind. + #LOC_BDArmory_WMWindow_MultiMissileTgtNum_desc = Wenn mehrere gelenkte Raketen vorhanden sind, bestimmt dieser Wert, wie viele verschiedene Ziele die KI mit Raketen angreifen wird. Sobald die maximale Zahl (definiert durch "Raketen pro Ziel") abgefeuert wurde, schaltet die KI das nächste Ziel auf. + #LOC_BDArmory_WMWindow_MissilesTgt_desc = Raketen pro Ziel - Bestimmt, wie viele gelenkte Raketen auf das gleiche Ziel abgefeuert werden. Weitere Raketen werden erst dann auf das gleiche Ziel angefeuert, wenn die Raketen eingeschlagen sind oder anderweitig zerstört wurden. + #LOC_BDArmory_WMWindow_TargetType_desc = Erweiterte Zieleinstellungen - Diese Einstellungen bestimmen, auf welchen Bereich des feindlichen Flugzeugs/Gefährts das Waffen-Kontrollsystem zielt. + #LOC_BDArmory_WMWindow_EngageType_desc = Ziele - Dieses Menü erlaubt es, die Art der Ziele, die das Waffen-Kontrollsystem angreifen wird, zu bestimmen. Die Auswahl gilt für alle auf dem Gefährt/Flugzeug vorhandenen Waffen. Mehrfachauswahl möglich. + + #LOC_BDArmory_WMWindow_TargetPriority = Zielpriorität + #LOC_BDArmory_WMWindow_targetBias = Prio. für akt. Ziel + #LOC_BDArmory_WMWindow_targetPreference = Luftziele bevorzugen + #LOC_BDArmory_WMWindow_targetProximity = Entfernung zum Ziel + #LOC_BDArmory_WMWindow_targetAngletoTarget = Winkel zum Ziel + #LOC_BDArmory_WMWindow_targetAngleDist = Winkel / Distanz + #LOC_BDArmory_WMWindow_targetAccel = Z. Beschleunigung + #LOC_BDArmory_WMWindow_targetClosingTime = Zeit bis Ziel + #LOC_BDArmory_WMWindow_targetgunNumber = Zahl der Waffen + #LOC_BDArmory_WMWindow_targetMass = Ziel Gewicht + #LOC_BDArmory_WMWindow_targetAllies = Nicht angegriffen + #LOC_BDArmory_WMWindow_targetThreat = Ziel Bedrohung + #LOC_BDArmory_WMWindow_defendTeammate = Verteidige Verbündete + #LOC_BDArmory_WMWindow_targetVIP = Greife VIP an + #LOC_BDArmory_WMWindow_defendVIP = Verteidige VIP + + #LOC_BDArmory_WMWindow_Prioritues_Desc = Zielpriorität - In diesem Menü wird die Zielpriorität eingestellt. + #LOC_BDArmory_WMWindow_targetBias_desc = Priorität für aktuelles Ziel - Bestimmt, wie stark das aktuelle Ziel gegenüber anderen potenziellen Zielen priorisiert wird. Bei einem hohen Wert wird das aktuelle Ziel bevorzugt (bis es zerstört ist). + #LOC_BDArmory_WMWindow_targetPreference_desc = Ziel-Typ-Präferenz - Bestimmt, welche Art Ziel die KI bevorzugt angreift. Je niedriger der Wert, umso mehr bevorzugt die KI Bodenziele. Je höher der Wert, desto mehr bevorzugt die KI Luftziele. + #LOC_BDArmory_WMWindow_targetProximity_desc = Entfernung zum Ziel - Stellt ein, ob Ziele in geringer oder in großer Entfernung priorisiert werden. Hoher Wert = nahe Ziele werden bevorzugt / Niedriger oder negativer Wert = weit entfernte Ziele werden bevorzugt. + #LOC_BDArmory_WMWindow_targetAngletoTarget_desc = Winkel zum Ziel - Bestimmt, wie stark geradeaus vorausliegende Ziele bevorzugt werden. + #LOC_BDArmory_WMWindow_targetAngleDist_desc = Winkel / Distanz - Bestimmt, wie stark der Quotient von Winkel und Entfernung die Priorität des Ziels beeinflusst. Ein hoher Wert führt zu Priorisierung von Zielen, die direkt voraus in geringer Entfernung liegen. + #LOC_BDArmory_WMWindow_targetAccel_desc = Z. Beschleunigung - Ein hoher Wert führt zu Priorisierung von Zielen, die schnell beschleunigen können (großes Verhältnis Schub zu Gewicht). Ein geringer oder negativer Wert führt zu Priorisierung von Zielen, die weniger schnell beschleunigen können. + #LOC_BDArmory_WMWindow_targetClosingTime_desc = Zeit bis Ziel - Ein hoher Wert führt zur Selektion von Zielen, die schneller erreicht werden können. + #LOC_BDArmory_WMWindow_targetgunNumber_desc = Zahl der Waffen - Ein hoher Wert führt zur Bevorzugung von Zielen, die (noch) mit vielen Waffen ausgerüstet sind. Ein Geringer oder negativer Wert bevorzugt Ziele mit wenigen Waffen. + #LOC_BDArmory_WMWindow_targetMass_desc = Ziel Gewicht - Der Wert bestimmt, Ziele welcher Masse (Gewicht) bevorzugt werden. Ein hoher Wert führt zur Bevorzugung von Zielen, die eine hohe Masse haben. + #LOC_BDArmory_WMWindow_targetDmg_desc = Ziel Beschädigung - Ein hoher Wert führt zu Bevorzugung von Zielen, die stärker Beschädigt sind (weniger verbleibende Lebenspunkte haben). + #LOC_BDArmory_WMWindow_targetAllies_desc = Nicht angegriffen - Ein hoher Wert führt zur Bevorzugung von Zielen, die nicht von Verbündeten angegriffen werden. Ein niedriger oder negativer Wert führt zur Bevorzugung von Zielen, die bereits von Verbündeten angegriffen werden. + #LOC_BDArmory_WMWindow_targetThreat_desc = Ziel Bedrohung - Bei einem hohen Wert werden vorrangig Ziele ausgewählt, die auf das eigene Geführt/Flugzeug schießen. Bei niedrigen oder negativen Werten werden diese ignoriert. + #LOC_BDArmory_WMWindow_targetVIP_desc = Greife VIP an / Verteidige VIP - Im Fall von Verbündeten oder Gegnern mit VIP Status: Ein hoher Wert erhöht die Priorität des VIP, sodass dieser mit höherer Wahrscheinlichkeit angegriffen oder verteidigt wird. Bei niedrigen oder negativen Werten werden VIPs ignoriert. + + #LOC_BDArmory_Settings_Title = BDArmory Einstellungen + #LOC_BDArmory_Settings_GeneralSettingsToggle = Generelle Einstellungen + #LOC_BDArmory_Settings_GraphicsSettingsToggle = Grafik / Oberfläche + #LOC_BDArmory_Settings_SliderSettingsToggle = Einstellungen + #LOC_BDArmory_Settings_RadarSettingsToggle = Radar-Einstellungen + #LOC_BDArmory_Settings_GameModesSettingsToggle = Spiel-Modi + #LOC_BDArmory_Settings_OtherSettingsToggle = Andere Einstellungen + #LOC_BDArmory_Settings_CompSettingsToggle = Wettkampf-Einstellungen + #LOC_BDArmory_Settings_GMSettingsToggle = Spielleiter-Einstellungen + #LOC_BDArmory_Settings_DebugSettingsToggle = Debug-Einstellungen + #LOC_BDArmory_Settings_AIToolbarButton = KI Symbolleisten-Schaltfläche + #LOC_BDArmory_Settings_Instakill = Sofortige Zerstörung + #LOC_BDArmory_Settings_InfiniteAmmo = Unendliche Munition + #LOC_BDArmory_Settings_InfiniteMissiles = Unendliche Raketen + #LOC_BDArmory_Settings_BulletFX = Schuss-Grafikeffekte + #LOC_BDArmory_Settings_BulletHits = Einschusslöcher + #LOC_BDArmory_Settings_EjectShells = Sichtbarer Patronen-Auswurf + #LOC_BDArmory_Settings_VesselRelativeBulletChecks = Flugzeug-spezifischer Bezugsrahmen für Trefferbestimmung + #LOC_BDArmory_Settings_AimAssist = Zielhilfe + #LOC_BDArmory_Settings_GUIBackgroundOpacity = Fenster-Transparenz + #LOC_BDArmory_Settings_DrawAimers = Ziellinien anzeigen + #LOC_BDArmory_Settings_DebugTelemetry = Telemetrie anzeigen + #LOC_BDArmory_Settings_DebugLines = Linien anzeigen + #LOC_BDArmory_Settings_DebugAI = KI + #LOC_BDArmory_Settings_DebugArmor = Panzerung + #LOC_BDArmory_Settings_DebugCompetition = Wettkampf + #LOC_BDArmory_Settings_DebugDamage = Schaden + #LOC_BDArmory_Settings_DebugMissiles = Lenkraketen + #LOC_BDArmory_Settings_DebugOther = Anderes + #LOC_BDArmory_Settings_DebugRadar = Detektoren + #LOC_BDArmory_Settings_DebugSpawning = Flugzeuge starten + #LOC_BDArmory_Settings_DebugWeapons = Waffen + #LOC_BDArmory_Settings_RemoteFiring = Ferngesteuertes Feuern + #LOC_BDArmory_Settings_ClearanceCheck = Bombenauswurf Sicherheitscheck + #LOC_BDArmory_Settings_AmmoGauges = Munitions-Füllstand anzeigen + #LOC_BDArmory_Settings_GaplessParticleEmitters = Unterbrechungsfreie Partikelemitter + #LOC_BDArmory_Settings_FlareSmoke = IR-Täuschkörper-Rauch + #LOC_BDArmory_Settings_ShellCollisions = Patronen kollidieren + #LOC_BDArmory_Settings_BulletHoleDecals = Einschusslöcher anzeigen + #LOC_BDArmory_Settings_PerformanceLogging = Leistung protokollieren + #LOC_BDArmory_Settings_StrictWindowBoundaries = BDA Fenster auf dem Monitor halten + #LOC_BDArmory_Settings_PersistentFX = Grafikeffekte persistieren + #LOC_BDArmory_Settings_DisableKillTimer = Gelandete Flugzeuge warden nicht entfernt + #LOC_BDArmory_Settings_TraceVessels = Pfadaufzeichnung automatisch starten + #LOC_BDArmory_Settings_TraceVesselsManualStart = Pfadaufzeichnung starten + #LOC_BDArmory_Settings_TraceVesselsManualStop = Pfadaufzeichnung stoppen + #LOC_BDArmory_Settings_ShowEditorSubcategories = Zweite Symbollesite im Baumenü anzeigen + #LOC_BDArmory_Settings_AutocategorizeParts = BDA Teile automatisch sortieren + #LOC_BDArmory_Settings_waterDrag = Wasserwiderstand simulieren + #LOC_BDArmory_Settings_AutoLoadToKSC = Bei Spielstart direkt ins KSC + #LOC_BDArmory_Settings_GenerateCleanSave = Saubere Speicherstände generieren + #LOC_BDArmory_Settings_AutoDisableUI = BDA Anzeige automatisch verstecken + #LOC_BDArmory_Settings_AutoResumeTournaments = Turniere automatisch fortsetzen + #LOC_BDArmory_Settings_AutoQuitAtEndOfTournament = Spiel beenden wenn Turnier beended + #LOC_BDArmory_Settings_AutoQuitMemoryUsage = Spiel beenden wenn Speicher belegt + #LOC_BDArmory_Settings_CurrentMemoryUsageEstimate = Aktueller Speicherverbrauch + #LOC_BDArmory_Settings_TimeOverride = Zeit verlangsamen/beschleunigen + #LOC_BDArmory_Settings_TimeScale = Faktor + #LOC_BDArmory_Settings_legacyArmor = Panzerung berechnen nach alter Methode + #LOC_BDArmory_Settings_AdvancedUserSettings = Einstellungen für Fortgeschrittene + #LOC_BDArmory_Settings_DamageMultiplier = Schadens-Multiplikator + #LOC_BDArmory_Settings_ExtraDamageSliders = Weitere Schaden-Einstellungen + #LOC_BDArmory_Settings_BallisticDamageMultiplier = Bballistischer Schaden + #LOC_BDArmory_Settings_ExplosiveDamageMultiplier = Explosivschaden + #LOC_BDArmory_Settings_RocketExplosiveDamageMultiplier = Raketen-Explosivschaden + #LOC_BDArmory_Settings_MissileExplosiveDamageMultiplier = Lenkraketen-Explosivschaden + #LOC_BDArmory_Settings_ArmorExplosivePenetrationResistanceMultiplier = Resistenz von Panzerung gegen Explosivschaden (Multiplikator) + #LOC_BDArmory_Settings_ExplosiveBattleDamageMultiplier = Kampfschaden-Explosionen + #LOC_BDArmory_Settings_ImplosiveDamageMultiplier = Implosionsschaden + #LOC_BDArmory_Settings_SecondaryEffectDuration = Spezialwaffen Schaden-Dauer + #LOC_BDArmory_Settings_BallisticTrajectorSimulationMultiplier = Schaden durch ballistische Flugbahn + #LOC_BDArmory_Settings_DebrisCleanUpDelay = Trümmer entfernen nach + #LOC_BDArmory_Settings_ArmorMassMultiplier = Panzerung-Masse Multiplikator + #LOC_BDArmory_Settings_Scoring_HeadShot = Hinrichtung Zeitlimit + #LOC_BDArmory_Settings_Scoring_KillSteal = Abstauber Zeitlimit + #LOC_BDArmory_Settings_MaxBulletHoles = Max. Zahl Einschusslöcher + #LOC_BDArmory_Settings_TerrainAlertFrequency = Frequenz der Geländeverfolgung + #LOC_BDArmory_Settings_CameraSwitchFrequency = Kameraperspektive wechseln nach + #LOC_BDArmory_Settings_DeathCameraInhibitPeriod = Leichenschau-Kamera wechseln nach + #LOC_BDArmory_Settings_Max_PWing_HP = Max. Leben Skalierung Schwelle + #LOC_BDArmory_Settings_HP_Clamp = Max. Leben Limit + #LOC_BDArmory_Settings_PWingsHack = Prozedurale Flügel-Kanten (Edges) erzeugen Auftrieb + #LOC_BDArmory_Settings_DisableRamming = Rammen deaktivieren + #LOC_BDArmory_Settings_DefaultFFATargeting = Zielsuche nach alter Methode + #LOC_BDArmory_Settings_TagMode = Fangenspiel-Modus + #LOC_BDArmory_Settings_PaintballMode = Paintball-Modus + #LOC_BDArmory_Settings_DumbIRSeekers = Dumme IR-Lenkraketen + #LOC_BDArmory_Settings_RunwayProject = Runway Project + #LOC_BDArmory_Settings_RunwayProjectRound = Runway Project Runde + #LOC_BDArmory_Settings_BattleDamage = Kampfschaden + #LOC_BDArmory_Settings_GravityHacks = Abschuss erhöht Schwerkraft + #LOC_BDArmory_Settings_AutoEnableVesselSwitching = Automatischer Kamerawechsel + #LOC_BDArmory_Settings_AutonomousCombatSeats = Autonomer Externer Pilotensitz + #LOC_BDArmory_Settings_DestroyWMWhenNotControlled = Verwaiste Waffen-Kontrollsysteme zerstören + #LOC_BDArmory_Settings_DisplayCompetitionStatus = Wettkampf-Status anzeigen + #LOC_BDArmory_Settings_DisplayCompetitionStatusHiddenUI = Wettkampf-Status bei F2 weiter anzeigen + #LOC_BDArmory_Settings_ScrollZoomPrevention = Mausrad-Zoom verhindern + #LOC_BDArmory_Settings_CameraSwitchIncludeMissiles = Kamera-Umschaltung: inkl. Raketen + #LOC_BDArmory_Settings_ResetHP = Trefferpunkte auf Standard zurücksetzen + #LOC_BDArmory_Settings_ResetArmor = Verteidigungspunkte auf Standard zurücksetzen + #LOC_BDArmory_Settings_ResetHull = Materialien auf Standard (Alu) zurücksetzen + #LOC_BDArmory_Settings_IntakeHack = Lufteinlässe funktionieren ohne Sauerstoff + #LOC_BDArmory_Settings_PWingsThickHP = Dicke beeinflusst Masse von prozed. Flügeln + #LOC_BDArmory_Settings_KerbalSafety = Kerbal-Sicherheit + #LOC_BDArmory_Settings_KerbalSafetyInventory = Kerbal-Ausstattung + #LOC_BDArmory_Settings_KerbalSafetyInventory_NoChange = Nicht ändern + #LOC_BDArmory_Settings_KerbalSafetyInventory_ResetDefault = Auf Standard zurücksetzen + #LOC_BDArmory_Settings_KerbalSafetyInventory_ChuteOnly = Nur Fallschirm + #LOC_BDArmory_Settings_PeaceMode = Nur verfolgen, nicht schießen + #LOC_BDArmory_settings_FireRate = Feuer-Rate frei einstellen + #LOC_BDArmory_settings_FireRateCenter = Feuer-Rate Durchschnitt + #LOC_BDArmory_settings_FireRateSpread = Feuer-Rate Abweichung + #LOC_BDArmory_settings_FireRateBias = Dämpfung der veränderlichen Feuer-Rate + #LOC_BDArmory_settings_FireRateHitMultiplier = Feuer-Rate Multiplikator pro Treffer + #LOC_BDArmory_settings_ZombieMode = Zombie Modus + #LOC_BDArmory_settings_zombieDmgMod = Zombie Schadensmultiplikator (wenn keine Hinrichtung) + + #LOC_BDArmory_Settings_BDSettingsToggle = Kampfschaden-Einstellungen + #LOC_BDArmory_Settings_BD_Proc = Häufigkeit + #LOC_BDArmory_Settings_BD_Engines = Antriebssystem + #LOC_BDArmory_Settings_BD_Prop_Dmg_Mult = Schaden (Faktor) + #LOC_BDArmory_Settings_BD_Prop_floor = Schub Minimum + #LOC_BDArmory_Settings_BD_Prop_flameout = Triebwerksausfall + #LOC_BDArmory_Settings_BD_Intakes = Lufteinlass + #LOC_BDArmory_Settings_BD_Gimbals = Schubvektor + #LOC_BDArmory_Settings_BD_Aero = Tragflächen + #LOC_BDArmory_Settings_BD_Aero_Dmg_Mult = Schaden (Faktor) + #LOC_BDArmory_Settings_BD_CtrlSrf = Leitwerk/Ruderschaden + #LOC_BDArmory_Settings_BD_Command = Steuerung und Kontrolle + #LOC_BDArmory_Settings_BD_PilotKill = Piloten können sterben + #LOC_BDArmory_Settings_BD_Tanks = Treibstofftank + #LOC_BDArmory_Settings_BD_Leak_Rate = Leckrate + #LOC_BDArmory_Settings_BD_Leak_Time = Leck Dauer + #LOC_BDArmory_Settings_BD_SubSystems = Subsysteme + #LOC_BDArmory_Settings_BD_Ammo = Munition + #LOC_BDArmory_Settings_BD_Volatile_Ammo = Munitionskisten explodieren wenn zerstört + #LOC_BDArmory_Settings_BD_Ammo_Mult = Explosionsschaden + #LOC_BDArmory_Settings_BD_Fires = Feuer + #LOC_BDArmory_Settings_BD_DoT = Feuerschaden + #LOC_BDArmory_Settings_BD_Fire_Dmg = Schaden pro Sekunde + #LOC_BDArmory_Settings_BD_FireHeat = Feuer erzeugen Hitze + #LOC_BDArmory_Settings_BD_FuelFireEX = Treibstoffexplosionen + #LOC_BDArmory_Settings_BD_ZombieMode = Kampfschaden + + #LOC_BDArmory_Settings_Adv_Targeting = Ziele + #LOC_BDArmory_Selecttargeting = Ziel-Optionen auswählen + #LOC_BDArmory_targetSetting = Ziele + #LOC_BDArmory_TargetCOM = Massenzentrum + #LOC_BDArmory_Weapons = Waffen + #LOC_BDArmory_Engines = Antrieb + #LOC_BDArmory_Command = Cockpit + #LOC_BDArmory_Mass = Schwerstes Bauteil + #LOC_BDArmory_Random = Zufälliges Bauteil + + #LOC_BDArmory_Ammo_Setup = Konfiguration der Munitionsladung + #LOC_BDArmory_Ammo_Weapon = Ausgewählte Waffe: + #LOC_BDArmory_Ammo_Belt = Aktueller Munitionsgürtel: + #LOC_BDArmory_advanced = Fortgeschrittene Munitionsauswahl + #LOC_BDArmory_simple = Einfache Munitionsauswahl + #LOC_BDArmory_useBelt = Benutzerdefinierter Munitionsgürtel: + #LOC_BDArmory_save = Speichern + #LOC_BDArmory_reset = Zurücksetzen + + // Vessel Spawner + #LOC_BDArmory_BDAVesselSpawner_Title = BDA Turnier-Starter + #LOC_BDArmory_Settings_SpawnOptions = Start-Einstellungen + #LOC_BDArmory_Settings_VesselSpawnGeoCoords = Turnier Start hier + #LOC_BDArmory_Settings_SpawnDistanceFactor = Start-Abstand Faktor + #LOC_BDArmory_Settings_SpawnDistance = Start-Abstand + #LOC_BDArmory_Settings_SpawnDistanceToggle = Absoluter Abstand oder Faktor + #LOC_BDArmory_Settings_SpawnReassignTeams = Teams neu zusammenstellen + #LOC_BDArmory_Settings_SpawnEaseInSpeed = Sanftes Absenken (Bodenstart) + #LOC_BDArmory_Settings_SpawnConcurrentVessels = Gleichzeitige Gegner (KS) + #LOC_BDArmory_Settings_SpawnLivesPerVessel = Leben pro Gegner (KS) + #LOC_BDArmory_Settings_SpawnDumpLogsEverySpawn = Ergebnisse speichern bei jedem Start (KS) + #LOC_BDArmory_Settings_SpawnContinueSingleSpawning = Einzelnes Fahrzeug kontinuierlich starten (S) + #LOC_BDArmory_Settings_SpawnRandomOrder = Zufällige Startreihenfolge (S) + #LOC_BDArmory_Settings_SpawnSpawnProbeHere = Start-Teil hier Starten + #LOC_BDArmory_Settings_SpawnStartCompetitionAutomatically = Wettkampf automatisch starten + #LOC_BDArmory_Settings_OutOfAmmoKillTime = Entfernen wenn Munition leer nach (KS) + #LOC_BDArmory_Settings_ClearDebrisNow = Jetzt Trümmer beseitigen + #LOC_BDArmory_Settings_ClearBystandersNow = Jetzt Zuschauer entfernen + #LOC_BDArmory_Settings_SaveSpawnLoc = Koordinaten Speichern + #LOC_BDArmory_Settings_WarpHere = Zu Startkoordinaten wechseln + #LOC_BDArmory_Settings_Planet = Planeten/Mond auswählen + + #LOC_BDArmory_Settings_SpawnFillSeats = Sitze füllen + #LOC_BDArmory_Settings_SpawnFillSeats_Minimal = Nur Pilot + #LOC_BDArmory_Settings_SpawnFillSeats_Default = Cockpits/Pilotensitz + #LOC_BDArmory_Settings_SpawnFillSeats_AllControlPoints = Alle Steuerplätze + #LOC_BDArmory_Settings_SpawnFillSeats_Cabins = Auch Kabinen + #LOC_BDArmory_Settings_Teams = Teams + #LOC_BDArmory_Settings_Teams_FFA = Ein Team pro Fahrzeug + #LOC_BDArmory_Settings_Teams_Folders = Ein Team pro Ordner/Datei + #LOC_BDArmory_Settings_Teams_SplitEvenly = In Teams aufteilen: + #LOC_BDArmory_Settings_Teams_Custom_Template = Benutzerdefinierte Vorlage + #LOC_BDArmory_Settings_SpawnFilesLocation = Fahrzeug-Dateien Unterordner + + #LOC_BDArmory_Settings_SpawnLocations = Interessante Orte + #LOC_BDArmory_Settings_SingleSpawn = Einzlne Runde starten + #LOC_BDArmory_Settings_ContinuousSpawning = Kontionuierlich starten (KS) + #LOC_BDArmory_Settings_CancelSpawning = KS abbrechen + + // Vessel Mover + #LOC_BDArmory_VesselMover_Title = BDA Vessel Mover + #LOC_BDArmory_VesselMover_MoveVessel = Fahrzeug verschieben + #LOC_BDArmory_VesselMover_SpawnVessel = Fahrzeug laden + #LOC_BDArmory_VesselMover_RecoverVessel = Fahrzeug wiederherstellen + #LOC_BDArmory_VesselMover_ChooseCrew = Besatzung auswählen + #LOC_BDArmory_VesselMover_PlaceVessel = Fahrzeug platzieren + #LOC_BDArmory_VesselMover_DropVessel = Fahrzeug absetzen + #LOC_BDArmory_VesselMover_InstantLowering = Sofortiges Absenken + #LOC_BDArmory_VesselMover_ClassicChooser = Klassische Fahrzeug-Dateiauswahl + #LOC_BDArmory_VesselMover_EnableBrakes = Bremsen aktievieren + #LOC_BDArmory_VesselMover_EnableSAS = SAS Aktivieren + #LOC_BDArmory_VesselMover_MinLowerSpeed = Minimale Absenkgeschwindigkeit + #LOC_BDArmory_VesselMover_LowerFast = Platzieren-Absenken + #LOC_BDArmory_VesselMover_BelowWater = Unter Wasser + #LOC_BDArmory_VesselMover_DontWorryAboutCollisions = Kollisionen nicht vermeiden + #LOC_BDArmory_VesselMover_Any = Alle + #LOC_BDArmory_VesselMover_ReallyRemoveKerbals = Kerbals wirklich entfernen? + #LOC_BDArmory_VesselMover_Help_Movement = Verschieben + #LOC_BDArmory_VesselMover_Help_Roll = Rollen + #LOC_BDArmory_VesselMover_Help_Pitch = Nicken + #LOC_BDArmory_VesselMover_Help_Yaw = Gieren + #LOC_BDArmory_VesselMover_Help_AutoRotateRocket = Rakete autom. drehen + #LOC_BDArmory_VesselMover_Help_AutoRotatePlane = Flugz. autom. drehen + #LOC_BDArmory_VesselMover_Help_CycleAltitudes = Voreinst. Höhe wechseln: Tab, Hochst.+Tab + #LOC_BDArmory_VesselMover_Help_ResetAltitude = Höhe zurücksetzen + #LOC_BDArmory_VesselMover_Help_AdjustAltitude = Höhe einstellen + + // Waypoints game mode + #LOC_BDArmory_Settings_WaypointsMode = Wegpunkt-Modus + #LOC_BDArmory_Settings_WaypointsOptions = Wegpunkt-Optionen + #LOC_BDArmory_Settings_WaypointsOneAtATime = Einer nach dem anderen + #LOC_BDArmory_Settings_WaypointsInfFuelAtStart = Kein Treibstoffvervbrauch bis zum ersten Wegpunkt + #LOC_BDArmory_Settings_WaypointsShow = Wegpunkte grafisch anzeigen + + // Tournaments + #LOC_BDArmory_Settings_TournamentOptions = Turnier-Einstellungen + #LOC_BDArmory_Settings_TournamentStyle = Turnier-Stil + #LOC_BDArmory_Settings_TournamentDelayBetweenHeats = Pause zwischen den Läufen + #LOC_BDArmory_Settings_TournamentTimeWarpBetweenRounds = Zeit vorspulen zw. Läufen + #LOC_BDArmory_Settings_TournamentRounds = Runden + #LOC_BDArmory_Settings_TournamentVesselsPerHeat = Fahrzeuge pro Lauf + #LOC_BDArmory_Settings_TournamentVesselsPerTeam = Fahrzeuge pro Team pro Lauf + #LOC_BDArmory_Settings_TournamentTeamsPerHeat = Teams pro Lauf + #LOC_BDArmory_Settings_GauntletOpponentsFilesLocation = Spießrutenlauf Gegner-Dateien + #LOC_BDArmory_Settings_TournamentOpponentTeamsPerHeat = Opponent Teams Per Heat + #LOC_BDArmory_Settings_TournamentOpponentVesselsPerTeam = Opponent Vessels Per Team + #LOC_BDArmory_Settings_TournamentFullTeams = Fahrzeuge mehrfach verwenden, um Teams zuu füllen + #LOC_BDArmory_Settings_TournamentSetup = Turnier aufsetzen + #LOC_BDArmory_Settings_TournamentRun = Turnier starten + #LOC_BDArmory_Settings_TournamentStop = Turnier beenden + + // Custom Spawn Templates + #LOC_BDArmory_Settings_CustomSpawnTemplateOptions = Benutzerdefinierte Vorlage + #LOC_BDArmory_Settings_SpawnOnly = Nur laden + #LOC_BDArmory_Settings_SpawnAndStartCompetition = Laden und Wettkampf starten + #LOC_BDArmory_Settings_CustomSpawnTemplate_ReplaceTeam = Teams ersetzen + #LOC_BDArmory_Settings_CustomSpawnTemplate_InvalidParts = UNGÜLTIGE TEILE + #LOC_BDArmory_Settings_CustomSpawnTemplate_UnknownModules = Module + #LOC_BDArmory_Settings_CustomSpawnTemplate_Clear = Entfernen + #LOC_BDArmory_Settings_CustomSpawnTemplate_ClearAll = Alles entfernen + #LOC_BDArmory_Settings_CustomSpawnTemplate_Refresh = Aktualisieren + #LOC_BDArmory_Settings_CustomSpawnTemplate_Parts = Teile + #LOC_BDArmory_Settings_CustomSpawnTemplate_Mass = Gewicht + #LOC_BDArmory_Settings_CustomSpawnTemplate_Version = Version + #LOC_BDArmory_Settings_CustomSpawnTemplate_TemplateSelection = Vorlage auswählen + #LOC_BDArmory_Settings_CustomSpawnTemplate_CrewSelection = Besatzung auswählen + #LOC_BDArmory_Settings_CustomSpawnTemplate_VesselSelection = Fahrzeug auswählen + + // Observers + #LOC_BDArmory_Settings_Observers = Zuschauer + #LOC_BDArmory_ObserverSelection_Title = Zuschauer auswählen + #LOC_BDArmory_ObserverSelection_SelectAll = Alles auswählen + #LOC_BDArmory_ObserverSelection_SelectNone = Nichts auswählen + + #LOC_BDArmory_Settings_HeartBleed = Verschleiß + #LOC_BDArmory_Settings_HeartBleedRate = Verschleiß Rate + #LOC_BDArmory_Settings_HeartBleedInterval = Verschleiß Intervall + #LOC_BDArmory_Settings_HeartBleedThreshold = Verschleiß Grenze (% Leben) + + #LOC_BDArmory_Settings_ResourceSteal = Resourcen klauen + #LOC_BDArmory_Settings_ResourceSteal_RespectFlowStateIn = Fluss-Status einwärts respektieren + #LOC_BDArmory_Settings_ResourceSteal_RespectFlowStateOut = Fluss-Status auswärts respektieren + #LOC_BDArmory_Settings_FuelStealRation = Treibstoff Anteil + #LOC_BDArmory_Settings_AmmoStealRation = Munition Anteil + #LOC_BDArmory_Settings_CMStealRation = Gegenmaßnahmen Anteil + + #LOC_BDArmory_Settings_AsteroidField = Asteroiden-Feld + #LOC_BDArmory_Settings_AsteroidFieldNumber = Asteroiden Anzahl + #LOC_BDArmory_Settings_AsteroidFieldAltitude = Asteroiden-Feld Höhe + #LOC_BDArmory_Settings_AsteroidFieldRadius = Asteroiden-Feld Radius + #LOC_BDArmory_Settings_AsteroidFieldAnomalousAttraction = Anormale Anziehungskraft + #LOC_BDArmory_Settings_AsteroidRain = Asteroiden-Regen + #LOC_BDArmory_Settings_AsteroidRainNumber = Asteroiden Anzahl + #LOC_BDArmory_Settings_AsteroidRainAltitude = Asteroiden-Regen Höhe + #LOC_BDArmory_Settings_AsteroidRainRadius = Asteroiden-Regen Radius + #LOC_BDArmory_Settings_AsteroidRainFollowsCentroid = Folgt Fahrzeug Ort + #LOC_BDArmory_Settings_AsteroidRainFollowsSpread = Radius folgt Fahrzeug-Verteilung + + #LOC_BDArmory_Settings_RWRWindowScale = Radarwarner Fenstergröße + #LOC_BDArmory_Settings_RadarWindowScale = Radar Fenstergröße + #LOC_BDArmory_Settings_LogarithmicRWRDisplay = Logarithmische RWR Anzeige + #LOC_BDArmory_Settings_TargetWindowScale = Ziel-Fenster Größe + #LOC_BDArmory_Settings_TargetWindowInvertMouse = Maus invertieren im Ziel-Fenster + #LOC_BDArmory_Settings_TriggerHold = Auslöser halten + #LOC_BDArmory_Settings_UIVolume = Lautstärke d. BDA Soundeffekte + #LOC_BDArmory_Settings_WeaponVolume = Waffen-Lautstärke + + #LOC_BDArmory_Settings_CompetitionDistance = Kampf-Start Distanz + #LOC_BDArmory_Settings_CompetitionDuration = Runde/Lauf beenden nach + #LOC_BDArmory_Settings_CompetitionIntraTeamSeparation = Ibstand zwischen Team-Mitgliedern + #LOC_BDArmory_Settings_CompetitionIntraTeamSeparationPerMember = / Mitglied + #LOC_BDArmory_Settings_CompetitionFinalGracePeriod = Finale Gnadenfrist + #LOC_BDArmory_Settings_CompetitionInitialGracePeriod = Initiale Gnadenfrist + #LOC_BDArmory_Settings_CompetitionKillTimer = Gelandete Flugzeuge entfernen nach + #LOC_BDArmory_Settings_CompetitionNonCompetitorRemovalDelay = Zuschauer entfernen nach + #LOC_BDArmory_Settings_CompetitionKillerGMFrequency = Entfernen all X Sekunden + #LOC_BDArmory_Settings_CompetitionKillerGMGracePeriod = Gnadenfrist + #LOC_BDArmory_Settings_CompetitionAltitudeLimitHigh = Oberes Höhenlimit + #LOC_BDArmory_Settings_CompetitionAltitudeLimitLow = Unteres Höhenlimit + #LOC_BDArmory_Settings_CompetitionStarting = Starte den Wettkampf + #LOC_BDArmory_Settings_DogfightCompetition = Wettkampf + #LOC_BDArmory_Settings_StartCompetition = Starte Wettkampf + #LOC_BDArmory_Settings_StopCompetition = Wettkampf beenden + #LOC_BDArmory_Settings_StartCompetitionNow = Wettkampf JETZT starten + #LOC_BDArmory_Settings_CompetitionStartNowAfter = Wettkampf JETZT Verzögerung + #LOC_BDArmory_Settings_CompetitionStartDespiteFailures = Wettkampf starten trotz Fehler + #LOC_BDArmory_Settings_StartRapidDeployment = Schnelle Bereitstellung starten + #LOC_BDArmory_Settings_StartOrbitalDeployment = Orbitale Bereitstellung starten + #LOC_BDArmory_Settings_LowGravDeployment = Wettkampf mit Abheben bei niedriger Schwerkraft starten + #LOC_BDArmory_Settings_EditInputs = Tastenbelegung einstellen + #LOC_BDArmory_Settings_CompetitionCloseSettingsOnCompetitionStart = Einstellungsfenster schließen beim Wettkampfstart + + #LOC_BDArmory_BDARemoteOrchestration_Title = BDA ferngesteuerte Orchestrierung + #LOC_BDArmory_Settings_RemoteLogging = Ferngesteuerte Orchestrierung + #LOC_BDArmory_Settings_RemoteInterheatDelay = Pause zwischen den Läufen + #LOC_BDArmory_Settings_RemoteSync = Ferngesteuertes Turnier starten + #LOC_BDArmory_Settings_CompetitionID = Turnier ID Nummer + + //Evolution + #LOC_BDArmory_Evolution_Title = BDA Evolution + #LOC_BDArmory_Evolution_Options = Evolution Einstellungen + #LOC_BDArmory_Evolution_HeatsPerGroup = Läufe pro Gruppe + #LOC_BDArmory_Evolution_MutationsPerHeat = Mutationen pro Lauf + #LOC_BDArmory_Evolution_AdversariesPerHeat = Gegner pro Lauf + #LOC_BDArmory_Evolution_ID = Evolution + #LOC_BDArmory_Evolution_Status = Status + #LOC_BDArmory_Evolution_Group = Gruppe + #LOC_BDArmory_Evolution_Heat = Lauf + + #LOC_BDArmory_Generic_SaveandClose = Speichern und schließen + + #LOC_BDArmory_InputSettings_Weapons = Waffen + #LOC_BDArmory_InputSettings_TargetingPod = Zielerfassungssystem + #LOC_BDArmory_InputSettings_Radar = Radar + #LOC_BDArmory_InputSettings_VesselSwitcher = Fahrzeug-Übersicht + #LOC_BDArmory_InputSettings_Tournament = Turnier + #LOC_BDArmory_InputSettings_TimeScaling = Zeitraffer + #LOC_BDArmory_InputSettings_GUI = BDA-Fenster + #LOC_BDArmory_InputSettings_BackBtn = Zurück + #LOC_BDArmory_InputSettings_recordedInput = Taste drücken + #LOC_BDArmory_InputSettings_SetKey = Taste Einst. + #LOC_BDArmory_InputSettings_Clear = Zurücks. + + #LOC_BDArmory_ProtoStageIconInfo_Reloading = Nachladen + #LOC_BDArmory_ProtoStageIconInfo_Overheat = Überhitzung + #LOC_BDArmory_ProtoStageIconInfo_AmmoOut = Keine Munition + + #LOC_BDArmory_WingCommander_Title = Flügelmann + #LOC_BDArmory_WingCommander_Guiname1 = Formation Weite + #LOC_BDArmory_WingCommander_Guiname2 = Formation Verzögerung + #LOC_BDArmory_WingCommander_Guiname3 = Fenster ein/ausschalten + #LOC_BDArmory_WingCommander_SelectAll = Alle auswählen + #LOC_BDArmory_WingCommander_CommandSelf = Sich selbst stuern + #LOC_BDArmory_WingCommander_Follow = Folgen + #LOC_BDArmory_WingCommander_FlyToPos = Zur Startposition fliegen + #LOC_BDArmory_WingCommander_AttackPos = Zur Startposition fliegen + #LOC_BDArmory_WingCommander_ActionGroup = Aktionsgruppe + #LOC_BDArmory_WingCommander_ActionGroups = Aktionsgruppen + #LOC_BDArmory_WingCommander_TakeOff = Abheben/Starten + #LOC_BDArmory_WingCommander_Release = Formation auflösen + #LOC_BDArmory_WingCommander_FormationSettings = Formation einstellen + #LOC_BDArmory_WingCommander_Spread = Formation Weite + #LOC_BDArmory_WingCommander_Lag = Formation Verzögerung + #LOC_BDArmory_WingCommander_ScreenMessage = Zielkoordinaten wählen.\nRechtsklick zum Abbrechen + + #LOC_BDArmory_BDAVesselSwitcher_Title = BDA Fahrzeug-Übersicht + + //GUI Names + #LOC_BDArmory_EjectVelocity = Ausdrück-Kraft + #LOC_BDArmory_TNTMass = TNT Äquivalent + #LOC_BDArmory_BlastRadius = Explosionsradius + #LOC_BDArmory_WeaponName = Waffen-Name\u0020 + #LOC_BDArmory_GuidanceType = Zielführungs-Typ\u0020 + #LOC_BDArmory_TargetingMode = Zielführungs-Modus\u0020 + #LOC_BDArmory_ActiveRadarRange = Radar-Reichweite + + #LOC_BDArmory_Rails = Justierbare Raketenschiene + #LOC_BDArmory_IncreaseHeight = Höhe ++ + #LOC_BDArmory_DecreaseHeight = Höhe -- + #LOC_BDArmory_IncreaseLength = Länge ++ + #LOC_BDArmory_DecreaseLength = Länge -- + #LOC_BDArmory_RailsPlus = Schiene ++ + #LOC_BDArmory_RailsMinus = Schiene -- + + #LOC_BDArmory_PilotAI_PID = PID Regler + #LOC_BDArmory_SteerFactor = Stärke (P) + #LOC_BDArmory_SteerKi = Korrektur (I) + #LOC_BDArmory_SteerDamping = Dämpfung (D) + + #LOC_BDArmory_DynamicSteerDamping = Dynamische Steuerungs-Dämpfung + #LOC_BDArmory_DynamicDamping = Dynamische Dämpfung + #LOC_BDArmory_DynamicDampingMin = Dämpfung + #LOC_BDArmory_DynamicDampingMax = Auf-Ziel-Dämpfung + #LOC_BDArmory_DynamicDampingFactor = Umschaltfunktion + #LOC_BDArmory_3AxisDynamicSteerDamping = 3-Achsen dynamische Dämpfung + #LOC_BDArmory_DynamicDampingPitch = Dynamische Dämpfung Nicken + #LOC_BDArmory_DynamicDampingPitchMin = Dämpfung Nicken + #LOC_BDArmory_DynamicDampingPitchMax = Auf-Ziel-Dämpfung + #LOC_BDArmory_DynamicDampingPitchFactor = N. Umschaltfunktion + #LOC_BDArmory_DynamicDampingYaw = Dynamische Dämpfung Gieren + #LOC_BDArmory_DynamicDampingYawMin = Dämpfung Gieren + #LOC_BDArmory_DynamicDampingYawMax = Auf-Ziel-Dämpfung + #LOC_BDArmory_DynamicDampingYawFactor = G. Umschaltfunktion + #LOC_BDArmory_DynamicDampingRoll = Dynamische Dämpfung Rollen + #LOC_BDArmory_DynamicDampingRollMin = Dämpfung Rollen + #LOC_BDArmory_DynamicDampingRollMax = Auf-Ziel-Dämpfung + #LOC_BDArmory_DynamicDampingRollFactor = R. Umschaltfunktion + + #LOC_BDArmory_PIDAutoTune = PID Automatisch einstellen + #LOC_BDArmory_AutoTuningLoss = Verlustfunktion + #LOC_BDArmory_PIDAutoTuningNumSamples = Auto-Einst. Zahl der Messwerte + #LOC_BDArmory_PIDAutoTuningFastResponseRelevance = Auto-Einst. bevorzugt agiles Flugverhalten + #LOC_BDArmory_PIDAutoTuningInitialLearningRate = Auto-Einst. anfängliche Lernrate + #LOC_BDArmory_PIDAutoTuningSpeed = Auto-Einst. Geschwindigkeit + #LOC_BDArmory_PIDAutoTuningAltitude = Auto-Einst. Flughöhe + #LOC_BDArmory_PIDAutoTuningFixedP = Auto-Einst. mit fixiertem P + #LOC_BDArmory_PIDAutoTuningClampMaximums = Auto-Einst. berücksichtigen Maxima + + #LOC_BDArmory_TargetPriority = Ziel-Auswahl-Priorität + #LOC_BDArmory_TargetPriority_CurrentTarget = Aktuelles Ziel + #LOC_BDArmory_TargetPriority_TargetScore = Bewertung des Ziels + #LOC_BDArmory_TargetPriority_Settings = Ziel-Auswahl-Einstellungen + #LOC_BDArmory_TargetPriority_CurrentTargetBias = Bevorzuge aktuzelles Ziel + #LOC_BDArmory_TargetPriority_TargetProximity = Entfernung d. Ziels + #LOC_BDArmory_TargetPriority_AirVsGround = Luftziele bevorzugen + #LOC_BDArmory_TargetPriority_CloserAngleToTarget = Kleinerer Winkel zum Ziel + #LOC_BDArmory_TargetPriority_TargetAcceleration = Ziel Beschleunigung + #LOC_BDArmory_TargetPriority_ShorterClosingTime = In kurzerer Zeit erreicht + #LOC_BDArmory_TargetPriority_TargetWeaponNumber = Zahl der Waffen des Ziels + #LOC_BDArmory_TargetPriority_TargetMass = Gewicht des Ziels + #LOC_BDArmory_TargetPriority_TargetDmg = Schaden des Ziels + #LOC_BDArmory_TargetPriority_FewerTeammatesEngaging = Von weniger Teammitgliedern angegriffen + #LOC_BDArmory_TargetPriority_TargetThreat = Ziel greift eig. Fahrzeug an + #LOC_BDArmory_TargetPriority_AngleOverDistance = Winkel / Distanz + #LOC_BDArmory_TargetPriority_TargetProtectTeammate = Teammitglieder beschützen + #LOC_BDArmory_TargetPriority_TargetProtectVIP = Eigene VIPs beschützen + #LOC_BDArmory_TargetPriority_TargetAttackVIP = Feindliche VIPs angreifen + + #LOC_BDArmory_Countermeasure_Settings = Einstellungen Gegenmaßnahmen + #LOC_BDArmory_CMThreshold = Gegenmaßn. bei Zeit bis Einschlag + #LOC_BDArmory_CMRepetition = IR-Täuschkörper pro Abwehrsequenz + #LOC_BDArmory_CMInterval = IR-Täuschkörper Intervall + #LOC_BDArmory_CMWaitTime = IR-Täuschkörper Abwehrsequenz Pause + #LOC_BDArmory_ChaffRepetition = Düppel pro Abwehrsequenz + #LOC_BDArmory_ChaffInterval = Düppel Intervall + #LOC_BDArmory_ChaffWaitTime = Düppel Abwehrsequenz Pause + #LOC_BDArmory_ChaffFactor = Düppel Effektivität + + #LOC_BDArmory_IsVIP = Ist VIP? + #LOC_BDArmory_IsVIP_enabledText = Ja + #LOC_BDArmory_IsVIP_disabledText = Nein + + // Modular Missile, Custom Weapons + #LOC_BDArmory_StagesNumber = Raketenstufen + #LOC_BDArmory_StageToTriggerOnProximity = Stufe auslösen bei Annäherung + #LOC_BDArmory_RollCorrection = Roll-Korrektur + #LOC_BDArmory_RollCorrection_enabledText = Rollen + #LOC_BDArmory_RollCorrection_disabledText = Nicht Rollen + #LOC_BDArmory_TimeBetweenStages = Zeit zwischen Stufen + #LOC_BDArmory_MinSpeedGuidance = Minimalgeschwingdigkeit vor Kontrolle + #LOC_BDArmory_ClearanceRadius = Freihalten (Radius) + #LOC_BDArmory_ClearanceLength = Freihalten (Abstand) + #LOC_BDArmory_showRFGUI = Waffenbezeichnung ändern + #LOC_BDArmory_showRFGUI_enabledText = Waffenbezeichnung ändern + #LOC_BDArmory_showRFGUI_disabledText = GUI + + #LOC_BDArmory_PilotAI_Altitudes = Flughöhe + #LOC_BDArmory_DefaultAltitude = Standardhöhe (ü.G.) + #LOC_BDArmory_MinAltitude = Mindesthöhe (ü.G.) + #LOC_BDArmory_MaxAltitude = Maximale Höhe (ü.G.) + + #LOC_BDArmory_PilotAI_Speeds = Geschwindigkeit + #LOC_BDArmory_MaxSpeed = Höchstgeschwindigkeit + #LOC_BDArmory_TakeOffSpeed = Abhebegeschwindigkeit + #LOC_BDArmory_MinSpeed = Min. Geschw. im Kampf + #LOC_BDArmory_StrafingSpeed = Bodenangriff-Geschw. + #LOC_BDArmory_IdleSpeed = Reisegeschwindigkeit + #LOC_BDArmory_ABPriority = Nachbrenner-Priorität + #LOC_BDArmory_ABOverrideThreshold = Nachbrenner an wenn langsamer als + + #LOC_BDArmory_MaxDrift = Maximale Drift + + #LOC_BDArmory_PilotAI_ControlLimits = Kontroll-Limits + #LOC_BDArmory_SteerLimiter = Steuerung einschränken + #LOC_BDArmory_LowSpeedSteerLimiter = Faktor bei niedriger Geschw. + #LOC_BDArmory_LowSpeedLimiterSpeed = Niedrige Geschw. = + #LOC_BDArmory_HighSpeedSteerLimiter = Faktor bei hoher Geschw. + #LOC_BDArmory_HighSpeedLimiterSpeed = Hohe Geschw. = + #LOC_BDArmory_AltitudeSteerLimiterFactor = Faktor bei Großer Höhe + #LOC_BDArmory_AltitudeSteerLimiterAltitude = Große Höhe = + #LOC_BDArmory_AttitudeLimiter = Lagekontrolle Limit + #LOC_BDArmory_BankLimiter = Neigungswinkel Limit + #LOC_BDArmory_WaypointPreRollTime = Neigung vor Wegpunkt (s) + #LOC_BDArmory_WaypointYawAuthorityTime = Gieren am Wegpunkt (s) + #LOC_BDArmory_maxAllowedGForce = Maximale G-Kraft + #LOC_BDArmory_maxAllowedAoA = Maximaler Anstellwinkel + #LOC_BDArmory_postStallAoA = Post-Stall (Überziehen) Anstellwinkel + + #LOC_BDArmory_PilotAI_EvadeExtend = Ausweichen / Abstand Gewinnen + #LOC_BDArmory_ExtendMultiplier = Abstand gewinnen (Faktor *300m) + #LOC_BDArmory_ExtendDistanceAirToAir = Abstand im Luftkampf (m) + #LOC_BDArmory_ExtendAngleAirToAir = Ziel-Winkel im Luftkampf + #LOC_BDArmory_ExtendDistanceAirToGroundGuns = Abstand bei Bodenangriff mit Kanonen (m) + #LOC_BDArmory_ExtendDistanceAirToGround = Abstand bei Bodenangriff (m) + #LOC_BDArmory_ExtendTargetVel = Ziel-Geschwindigkeits-Faktor + #LOC_BDArmory_ExtendTargetAngle = Ziel-Winkel + #LOC_BDArmory_ExtendTargetDist = Angestrebter Ziel-Abstand + #LOC_BDArmory_ExtendAbortTime = Abstand gewinnen abbrechen nach x Sekunden + #LOC_BDArmory_ExtendToggle = Abstand gewinnen an/aus + #LOC_BDArmory_MinEvasionTime = Ausweichen min. Zeit + #LOC_BDArmory_EvasionNonlinearity = Haken Schlagen (Faktor) + #LOC_BDArmory_EvasionThreshold = Gegner zielt auf mich (Radius) + #LOC_BDArmory_EvasionTimeThreshold = Gegner zielt auf mich (Zeit) + #LOC_BDArmory_EvasionIgnoreMyTargetTargetingMe = Meinem Ziel nicht ausweichen + #LOC_BDArmory_CollisionAvoidanceThreshold = Bei Passage unter X m ausweichen + #LOC_BDArmory_CollisionAvoidanceLookAheadPeriod = X Sekunden vorausberechnen + #LOC_BDArmory_CollisionAvoidanceStrength = Ausweich-Stärke (*50°) + #LOC_BDArmory_StandoffDistance = Sicherheitsabstand + + #LOC_BDArmory_PilotAI_Terrain = Terrainvermeidung + #LOC_BDArmory_TurnRadiusTwiddleFactorMin = Flugzeug Wendekreis Faktor (min) + #LOC_BDArmory_TurnRadiusTwiddleFactorMax = Flugzeug Wendekreis Faktor (max) + #LOC_BDArmory_WaypointTerrainAvoidance = Terrainvermeidung am Wegpunkt + #LOC_BDArmory_TerrainAvoidanceCriticalAngle = Rückenflug-Terrainvermeidung kritischer Winkel + + #LOC_BDArmory_PilotAI_Ramming = Rammen + #LOC_BDArmory_ControlSurfaceLag = Steuerverzögerung beim Rammen + #LOC_BDArmory_AllowRamming = Rammen aktivieren + + #LOC_BDArmory_PilotAI_Ejection = Schleudersitz + #LOC_BDArmory_EjectOnImpendingDoom = Auswerfen, wenn dem Untergang geweiht + + #LOC_BDArmory_SliderResolution = Auflösung der Schieberegler + #LOC_BDArmory_UnclampTuning = Limits entfernen + #LOC_BDArmory_UnclampTuning_enabledText = Unlimitiert + #LOC_BDArmory_UnclampTuning_disabledText = Limitiert + + //Pilot AI GUI context labels + #LOC_BDArmory_AIWindow_title = KI Autopilot + #LOC_BDArmory_AIWindow_infoLink = Infolink + #LOC_BDArmory_AIWindow_NoAI = Keine KI am Fahrzeug! + #LOC_BDArmory_PilotAI_Misc = Diverses + #LOC_BDArmory_AIWindow_SteerMultLow = <-- Träge + #LOC_BDArmory_AIWindow_SteerMultHi = Nervös --> + #LOC_BDArmory_AIWindow_SteerKiLow = <-- Zu wenig Vorhalte + #LOC_BDArmory_AIWindow_SteerKiHi = Zu viel Vorhalte --> + #LOC_BDArmory_AIWindow_SteerDampLow = <-- Wackelig + #LOC_BDArmory_AIWindow_SteerDampHi = Steif --> + #LOC_BDArmory_AIWindow_DynDampMin = Dampfung Minumum + #LOC_BDArmory_AIWindow_DynDampMax = Dämpfung Maximum + #LOC_BDArmory_AIWindow_DynDampMult = Nichtlinearität der Umschaltfunktion + #LOC_BDArmory_AIWindow_PIDAutoTuningNumSamplesMin = <-- Ungenauer + #LOC_BDArmory_AIWindow_PIDAutoTuningNumSamplesMax = Genauer -> + #LOC_BDArmory_AIWindow_PIDAutoTuningFastResponseRelevanceMin = <- Bessere Dämpfung + #LOC_BDArmory_AIWindow_PIDAutoTuningFastResponseRelevanceMax = Agiler -> + #LOC_BDArmory_AIWindow_PIDAutoTuningInitialLearningRateContext = Diesen Wert verringern, wenn die PID-Änderungen zu groß sind + #LOC_BDArmory_AIWindow_PIDAutoTuningAltitudeContext = Versuchen ±Mindestflughöhe um diesen Wert zu bleiben + #LOC_BDArmory_AIWindow_PIDAutoTuningSpeedContext = Ziel-Geschwindigkeit + #LOC_BDArmory_AIWindow_DefAlt = Flughöhe in Ruhe + #LOC_BDArmory_AIWindow_MinAlt = KI versucht, über dieser Höhe zu bleiben + #LOC_BDArmory_AIWindow_MaxAlt = KI versucht, unter dieser Höhe zu bleiben + #LOC_BDArmory_AIWindow_maxSpeed = KI fliegt nie schneller als diese Geschwindigkeit + #LOC_BDArmory_AIWindow_takeoff = KI zieht bei dieser Geschwindigkeit die Nase hoch + #LOC_BDArmory_AIWindow_minSpeed = KI versucht über dieser Geschwindigkeit zu bleiben + #LOC_BDArmory_AIWindow_atkSpeed = + #LOC_BDArmory_AIWindow_idleSpeed = Reisegeschwindigkeit (nicht im Kampf) + #LOC_BDArmory_AIWindow_ABPriority = Häufigkeit der Nachbrenner-Nutzung verändern + #LOC_BDArmory_AIWindow_ABOverrideThreshold_Context = Nachbrenner aktivieren wenn Flugzeug langsamer als + #LOC_BDArmory_AIWindow_LSSL = Kontroll-Limit wenn langsamer als "Niedrige Geschwindigkeit" + #LOC_BDArmory_AIWindow_LSLS = + #LOC_BDArmory_AIWindow_HSSL = Kontroll-Limit wenn schneller als "Hohe Geschwindigkeit" + #LOC_BDArmory_AIWindow_HSLS = + #LOC_BDArmory_AIWindow_ASLF = Kontroll-Limit abhängig von der Flughöhe + #LOC_BDArmory_AIWindow_ASLA = Höhe, ab der die Kontrolle verringert wird + #LOC_BDArmory_AIWindow_WPPreRoll = Rollen vor Erreichen eines Wegpunktes + #LOC_BDArmory_AIWindow_WPYawAuth = Stärkeres Gieren beim Erreichen eines Wegpunktes + #LOC_BDArmory_AIWindow_bankLimit = Neigungswinkel Limit + #LOC_BDArmory_AIWindow_GForce = KI versucht, dieses G-Limit nicht zu überschreiten + #LOC_BDArmory_AIWindow_AoA = KI versucht, diesen Anstellwinkel nicht zu überschreiten + #LOC_BDArmory_AIWindow_AoAPostStall = Post-Stall Flugmodus wenn Anstellwinkel höher als + #LOC_BDArmory_AIWindow_MinEvade = Minimale Zeit, die die KI einem Schuss ausweicht + #LOC_BDArmory_AIWindow_EvExNonlin = Stärke des Haken-Schlagens beim Ausweichen + #LOC_BDArmory_AIWindow_evadeDist = Ausweichen beginnt, wenn Angreifer innerhalb dieses Radius zielt + #LOC_BDArmory_AIWindow_evadetimeDist = Ausweichen beginnt, wenn länger als diese Zeit im Schussfeld + #LOC_BDArmory_AIWindow_ColDist = Anderen Flugzeugen ausweichen, wenn sie näher kommen als dies + #LOC_BDArmory_AIWindow_ColTime = KI berechnet Flugbahnen voraus für diesen Zeitraum + #LOC_BDArmory_AIWindow_ColStrength = Stärke des Ausweichmanövers + #LOC_BDArmory_AIWindow_standoff = KI versucht Gegner ins Schussfeld zu bringen wenn Abstand größer also dies + #LOC_BDArmory_AIWindow_ExtendAngleAirToAir_Context = Steigflug-Winkel beim Abstand-Gewinnen + #LOC_BDArmory_AIWindow_ExtendDistanceAirToAir_Context = KI versucht, diesen Abstand zu erreichen + #LOC_BDArmory_AIWindow_ExtendDistanceAirToGroundGuns_Context = Abstand gewinnen bei Bodenangriff mit Kanonen + #LOC_BDArmory_AIWindow_ExtendDistanceAirToGround_Context = Abstand gewinnen bei Bodenangriff mit Bomben + #LOC_BDArmory_AIWindow_Extendvel = Gegner zu langsam, um ihn aus akt. Position ins Schussfeld zu bringen + #LOC_BDArmory_AIWindow_ExtendAbortTimeContext = Abbrechen nach x Sekunden wenn Abstand nich hergestellt werden kann + #LOC_BDArmory_AIWindow_ExtendAngle = Winkel, ab dem der Gegner nicht ins Schussfeld genommen werden kann + #LOC_BDArmory_AIWindow_ExtendDist = Abstand, bei dem der Gegner zu nah ist, um ihn ins Schussfeld zu bringen + #LOC_BDArmory_AIWindow_terrainMin = Wendekreis-Faktor, wenn Flugzeug dem Boden ausweichen kann, ohne zu rollen + #LOC_BDArmory_AIWindow_terrainMax = Wendekreis-Faktor, wenn Flugzeug beim Ausweichen auch rollen muss + #LOC_BDArmory_AIWindow_InvertedTerrainAvoidanceCriticalAngleContext = Krit. W. f. Terrainverm.: Erst rollen oder invertiert ausw. + #LOC_BDArmory_AIWindow_WaypointTerrainAvoidanceContext = Bereich und Stärke der Wegpunkt-Terrainvermeidung + #LOC_BDArmory_AIWindow_ramLag = Ramm-Trajektorie optimieren mit Steuerverzögerung + #LOC_BDArmory_AIWindow_orbit = Richtung beim Kreisen + #LOC_BDArmory_AIWindow_standby = KI übernimmt, wenn Ziel in den Überwachungskreis einfliegt + + //PilotAi GUI labels + #LOC_BDArmory_AIWindow_PIDAutoTuningNumSamples = Auto Einst. (A-E) #Messwerte + #LOC_BDArmory_AIWindow_PIDAutoTuningFastResponseRelevance = A-E Agiles Flugverhalten + #LOC_BDArmory_AIWindow_PIDAutoTuningInitialLearningRate = A-E Initiale Lernrate + #LOC_BDArmory_AIWindow_PIDAutoTuningSpeed = A-E Geschwindigkeit + #LOC_BDArmory_AIWindow_PIDAutoTuningAltitude = A-E Flughöhe + #LOC_BDArmory_AIWindow_PIDAutoTuningFixedP = A-E Festes P + #LOC_BDArmory_AIWindow_PIDAutoTuningClampMaximums = A-E Maxima berücksichtigen + #LOC_BDArmory_AIWindow_ControlLimits = Kontrolle + #LOC_BDArmory_AIWindow_SteerLimiter = Steuerung einschränken + #LOC_BDArmory_AIWindow_PIDAutoTuningFixedFields = A-E Felder fixieren + #LOC_BDArmory_AIWindow_ABOverrideThreshold = Nachbrenner an wenn langsamer als + #LOC_BDArmory_AIWindow_LowSpeedSteerLimiter = Faktor bei niedriger Geschw. + #LOC_BDArmory_AIWindow_LowSpeedLimiterSpeed = Niedrige Geschw. = + #LOC_BDArmory_AIWindow_HighSpeedSteerLimiter = Faktor bei hoher Geschw. + #LOC_BDArmory_AIWindow_HighSpeedLimiterSpeed = Hohe Geschw. = + #LOC_BDArmory_AIWindow_AltitudeSteerLimiterFactor = Große Höhe Steuerfaktor + #LOC_BDArmory_AIWindow_AltitudeSteerLimiterAltitude = Große Höhe + #LOC_BDArmory_AIWindow_WaypointPreRollTime = Neigung vor Wegpunkt (s) + #LOC_BDArmory_AIWindow_WaypointYawAuthorityTime = Stärker gieren am Wegpunkt (s) + #LOC_BDArmory_AIWindow_postStallAoA = Post-Stall Anstellwinkel + #LOC_BDArmory_AIWindow_EvadeExtend = Energie + #LOC_BDArmory_AIWindow_ExtendDistanceAirToAir = Abst. gew. Luft-Luft + #LOC_BDArmory_AIWindow_ExtendAngleAirToAir = Abst. gew. Winkel Luft-Luft + #LOC_BDArmory_AIWindow_ExtendDistanceAirToGroundGuns = Abst. gew. Luft-Boden (Kanone) + #LOC_BDArmory_AIWindow_ExtendDistanceAirToGround = Abst. gew. Luft-Boden (Bomben) + #LOC_BDArmory_AIWindow_ExtendTargetVel = Abst. gew. Geschw. Faktor + #LOC_BDArmory_AIWindow_ExtendAbortTime = Abst. gew. Abbr. nach x Sek. + #LOC_BDArmory_AIWindow_ExtendTargetAngle = Winkel zum Ziel + #LOC_BDArmory_AIWindow_ExtendTargetDist = Ziel-Abstand + #LOC_BDArmory_AIWindow_EvasionNonlinearity = Haken schlagen + #LOC_BDArmory_AIWindow_EvasionThreshold = Ausweich-Radius + #LOC_BDArmory_AIWindow_EvasionTimeThreshold = Ausweich-Verzögerung + #LOC_BDArmory_AIWindow_CollisionAvoidanceThreshold = Zusammenstoß-Abstand + #LOC_BDArmory_AIWindow_CollisionAvoidanceLookAheadPeriod = Zusammenstoß-Vorausberechnung + #LOC_BDArmory_AIWindow_CollisionAvoidanceStrength = Ausweich-Stärke + #LOC_BDArmory_AIWindow_StandoffDistance = Sicherheitsabstand + #LOC_BDArmory_AIWindow_Terrain = Terrain + #LOC_BDArmory_AIWindow_TurnRadiusMin = Flugzeug Wendekreis Faktor (min) + #LOC_BDArmory_AIWindow_TurnRadiusMax = Flugzeug Wendekreis Faktor (max) + #LOC_BDArmory_AIWindow_WaypointTerrainAvoidance = Wegpunkt-Terrainvermeidung + #LOC_BDArmory_AIWindow_ControlSurfaceLag = Steuerverzögerung + + //Pilot AI infolink + #LOC_BDArmory_AIWindow_PidHelp = Der PID Regler (Proportional Integral Derivative, Stärke Korrektur Dämpfung) berechnet die Differenz zwischen der aktuellen und der benötigten Steuereingabe zum Erreichen eines Ziels oder einer Lage. Das Flugzeug wird geflogen, indem die Steuereingabe unter Berücksichtigung der Parameter Kraft, Korrektur, und Dämpfung fortlaufend korrigiert wird. Die Parameter müssen üblicherweise alle drei an das aktuelle Flugzeug und die gewünschten Flugeigenschaften angepasst werden. Dabei wird mit dem Parameter Stärke begonnen und daraufhin Korrektur und Dämpfung so angepasst, dass das Flugzeug so schnell wie möglich, jedoch ohne Überschwinger agiert. + #LOC_BDArmory_AIWindow_PidHelp_SteerMult = Stärke (P) - Stärke der Steuereingaben. Wird dieser Parameter zu niedrig gewählt, dann wird der volle Bewegungsbereich der Ruder und Klappen nicht ausgenutzt. Zu hoch, und das Flugzeug übersteuert. + #LOC_BDArmory_AIWindow_PidHelp_SteerKi = Korrektur (I) - Die mittels Stärke und Dämpfung errechnete Steuereingabe hat immer eine Verzögerung und einen mehr oder weniger großen Fehler, der sich in Unter- oder Überschießen zeigt. Dies kann durch die richtige Wahl des Korrekturwertes kompensiert werden. Wird der Wert zu niedrig gewählt, erreicht das Flugzeug die angestrebte Lage nicht, und Schüsse landen hinter dem Ziel. Ein zu hoher Wert kann sich in zu starker Vorhalte äußern (Schüsse landen vor dem Ziel). + #LOC_BDArmory_AIWindow_PidHelp_Steerdamp = Dämpfung (D) - Die Steuereingabe muss vor dem Erreichen der angestrebten Lage reduziert werden, sonst oszilliert das Flugzeug zwischen verschiedenen Zuständen des Überschießens. Ein zu hoher Wert verlangsamt die Lageänderungen ggfs. so stark, dass die angestrebte Lage nie erreicht wird. + #LOC_BDArmory_AIWindow_PidHelp_Dyndamp = Dynamische Dämpfung ermöglicht die Verwendung verschieden starker Dämpfung je nach Winkel zum Ziel. Der Minimalwert wird verwendet, wenn der Winkel zum Ziel groß ist. Der Maximalwert wird verwendet, wenn die Ziel-Lage annähernd erreicht ist. Wird der Parameter Umschaltfunktion auf 1 gesetzt, wird die Dämpfung linear mit dem Winkel zum Ziel variiert. Bei höheren Werten bleibt die Dämpfung bei größerem Winkel zunächst länger niedrig und steigt dann vor dem Erreichen der Ziel-Lage stärker an (Exponentialfunktion). In vielen Fällen ist es sinnvoll, die Dynamische Dämpfung separat für die drei Achsen (Nicken, Rolle, Gieren) einzustellen. + #LOC_BDArmory_AIWindow_PidHelp_AutoTune = PID Auto-Einstellen - Aktiviert einen PID Tuning Modus, bei dem die KI die Gradientenabstiegsmethode verwendet, um die Geschwindigkeit und Präzision zu erhöhen, mit der Flugzeug eine Reihe von Richtungsänderungen fliegt. + #LOC_BDArmory_AIWindow_PidHelp_AutoTune_details = \n - Die zu minimierende Verlustfunktion ist ∫f(x,θ)dθ über den Bereich θ ∈ (30°,120°) (unter Verwendung der Mittelwert-Riemann-Summe), wobei f(x,θ) gleich\n ∫(δp²-(α+t²)/θ² + γ-δr²-(α+t)/100/θ)dt\n für die aktuellen PID-Werte (x) und die Kursänderung (θ) ist, wobei δp der Richtungsfehler, δr der Rollfehler, α die Bevorzugung agilen Flugverhaltens und γ die Rollrelevanz ist (die im Laufe der Zeit automatisch angepasst wird, um den Beitrag der Richtungs- und Rollfehler auszugleichen). \n - Verwendung: Sobald das Flugzeug in der Luft ist (und sich nicht im Kampf befindet), die Selbstoptimierung aktivieren. Schieberegler auf die gewünschten Werte einstellen (die Standardwerte sind vernünftige Ausgangspunkte und können im SPH voreingestellt werden; die Anpassung einiger Schieberegler startet die Selbstoptimierung neu), dann die Selbstoptimierung laufen lassen, bis sie automatisch stoppt (wenn die Lernrate (LR) auf unter 1e-3 sinkt). Die PID-Werte werden nun auf die Werte eingestellt, die den geringsten Verlust verursacht haben. Diese werden gespeichert, damit sie im SPH wiederhergestellt werden können. \n - Parameter: Zahl der Messwerte - Die Anzahl der Kursänderungen (θ), die in der Riemann-Summe verwendet werden; höhere Werte verringern das Rauschen im Gradienten. Agiles Flugverhalten (α) - Wie stark sollen die frühen Richtungs- und Rollfehler gewichtet werden. Größere Werte beschleunigen die Richtungsöänderungen auf Kosten leicht höherer Kursfehler. Höhe und Geschwindigkeit - Die Zielhöhe und -geschwindigkeit, die bei der Abstimmung verwendet werden sollen. Festes P - Behält die Stärke (P) fest bei und stimmt nur Korrektur (I) und Dämpfung (D) ab. Maxima berücksichtigen - Die ermittelten Werte werden innerhalb der Grenzen der Schieberegler gehalten.\n - Empfehlungen: 1. Höhe und Geschwindigkeit für die automatische Abstimmung auf die Werte einstellen, die im Kampf verwendet werden sollen. 2. 5-10-fache Zeitskalierung verwenden. 3. Bergiges Terrain vermeiden. 4. Abstimmung zunächst ohne dynamische Dämpfung durchführen. Ergebnis als Ausgangspunkt für Einstellen der dynamischen Dämpfung verwenden, wobei alle Dämpfungswerte auf den abgestimmten statischen Dämpfungswert und die dynamischen Dämpfungsfaktoren auf 1 gesetzt werden. 5. Da die PID-Werte (derzeit) für das Anfliegen von Festpunkten optimiert werden, ist der eingestellte I-Wert möglicherweise nicht optimal für Positions- und Lageänderungen im Kampf mit beweglichen Ziuelen. Ein etwas größerer I-Wert könnte wünschenswert sein. + + #LOC_BDArmory_AIWindow_AltHelp = Diese Einstellungen bestimmen die Flughöhe, die die KI in verschiedenen Situationen anstrebt. + #LOC_BDArmory_AIWindow_AltHelp_Def = Standardhöhe - Dies ist die Höhe über Grund, die die KI anstrebt, wenn kein Luftkampf stattfindet. + #LOC_BDArmory_AIWindow_AltHelp_min = Mindesthöhe - Bei unterschreiten diese Höhe über Grund geht das Flugzeug in den Steigflug über. + #LOC_BDArmory_AIWindow_AltHelp_max = Maximale Höhe - analog zur Mindesthöhe: Die Höhe über Grund, bei der die KI in den Sinkflug übergeht. + + #LOC_BDArmory_AIWindow_SpeedHelp = Diese Einstellungen bestimmen die Flug-Geschwindigkeit, die die KI in verschiedenen Situationen anstrebt. + #LOC_BDArmory_AIWindow_SpeedHelp_min = Höchstgeschwindigkeit und Mindestgeschwindigkeit im Kampf - Die KI strebt im Luftkampf immer die Höchstgeschwindigkeit an. Wird diese Geschwindigkeit überschritten, bremst die KI durch Schubreduktion und (falls vorhanden) Luftbremsen ab. Manövrierfähige Flugzeuge können die Höchstgeschwindigkeit im Luftkampf nicht halten. Sinkt die Geschwindigkeit unter den Wert für "Mindestgeschwindigkeit im Kampf", bricht das Flugzeug den Luftkampf ab und gewinnt Abstand (siehe Einstellungen für Abstand Gewinnen in der Kategorie "Energie"). + #LOC_BDArmory_AIWindow_SpeedHelp_takeoff = Abhebegeschwindigkeit - Wenn das Flugzeug bei Aktivierung der KI auf dem Boden ist, definiert die Abhebegeschwindigkeit die Geschwindigkeit, die das Flugzeug erreichen muss, bevor es beginnt, die Nase hochzuziehen. + #LOC_BDArmory_AIWindow_SpeedHelp_idle = Reisegeschwindigkeit - Definiert die Geschwindigkeit, die die KI vor dem Luftkampf beim Kreisen oder beim Fliegen zur Startposition anstrebt. + #LOC_BDArmory_AIWindow_SpeedHelp_gnd = Bodenangriff-Geschwindigkeit - Geschwindigkeit, die die KI anstrebt, wenn das Flugzeug Bodenziele angreift. Bei beweglichen Bodenzielen wir die relative Geschwindigkeit des Ziels zur Bodenangriff-Geschwindigkeit addiert. + #LOC_BDArmory_AIWindow_SpeedHelp_ABpriority = Nachbrenner-Priorität - Bestimmt, ab welcher benötigten Beschleunigung die KI den Nachbrenner aktiviert. + #LOC_BDArmory_AIWindow_SpeedHelp_ABOverrideThreshold = Geschwindigkeit, bei deren Unterschreiten der Nachbrenner aktiviert wird, wenn bereits maximaler Schub anliegt. + + #LOC_BDArmory_AIWindow_ControlHelp = Kontroll-Limits bestimmen die Stärke von Steuereingaben unter verschiedenen Bedingungen und Höchstwerte für Neigungswinkel und G-Kraft. + #LOC_BDArmory_AIWindow_ControlHelp_limiters = Steuerung je nach Geschwindigkeit einschränken - "Niedrige" und "Hohe Geschwindigkeit" segmentieren den Bereich der möglichen Geschwindigkeiten in drei (!) Bereiche: (1) Langsamer als "Niedrige Geschwindigkeit", (2) zwischen "Niedriger" und "Hoher Geschwindigkeit", und (3) schneller als "Hohe Geschwindigkeit". Bei Niedriger Geschwindigkeit wird die Stärke der Steuerung mit dem "Faktor bei niedriger Geschwindigkeit" multipliziert. Wenn das Flugzeug unter einer gewissen Geschwindigkeit zu Strömungsabriss oder Trudeln neigt, sollte dieser Faktor kleiner als 1 gewählt werden. Bei Hoher Geschwindigkeit wird die Stärke der Steuerung mit dem "Faktor bei hoher Geschwindigkeit" multipliziert. Die Auslenkwinkel aller Ruder, Klappen, und Schubvektorsysteme sollten üblicherweise so eingestellt sein, dass bei hoher Geschwingigkeit ein Faktor von 1 gewählt werden kann. Im Fall (2), zwischen der Niedrigen und der Hohen Geschwindigkeit wird linear zwischen den Faktoren für Niedrige und Hohe Geschwindigkeit interpoliert. + #LOC_BDArmory_AIWindow_ControlHelp_bank = Neigungswinkel - Stellt die maximale Schräglage ein, die das Flugzeug beim Manövrieren anstreben darf. Wird dieser Wert kleiner als 180 gewählt, wird die KI nicht weiter als die gewählte Gradzahl von der horizontalen Lage abweichen. + #LOC_BDArmory_AIWindow_ControlHelp_clamps = Maximale G-Kraft und Anstellwinkel - Maximale G-Kraft limitiert die Manöver der KI, sodass das angegebene G-Limit nicht überschritten werden sollte. Das zugrunde liegende Rechenmodell wertet hierfür die bisher bei diesem Flug erreichten G-Kräfte aus, und liegt oft daneben. Das gleiche gilt für den maximalen Anstellwinkel. Die KI wertet die Anstellwinkel aus, die beim bisherigen Flug vorgekommen sind, um die Steuereingaben so zu limitieren, dass der maximale Anstellwinkel nicht überschritten wird. Funktioniert nur in Sonderfällen. Ein Post-Stall Steuer-Modus wird aktiviert, sobald der maximale Anstellwinkel überschritten ist. + + #LOC_BDArmory_AIWindow_EvadeHelp = Die Einstellungen für Ausweichen und Abstand Gewinnen bestimmen, die die KI auf Bedrohungen reagiert (Kanonenfeuer, Raketen, andere Flugzeuge), und wie sie versucht, sich selbst relativ zu gegnerischen Flugzeugen zu positionieren. + #LOC_BDArmory_AIWindow_EvadeHelp_Evade = Ausweichen min. Zeit, Ausweich-Radius, Ausweich-Verzögerung, Meinem Ziel nicht ausweichen - Diese vier Einstellungen bestimmen, wann die KI einer Bedrohung ausweichen wird. Ausweichen min. Zeit: Mindest-Zeitdauer, für die die KI Ausweichmanöver durchführen wird. Ist die Bedrohung nach Ablauf dieser Dauer noch aktiv, kann die KI erneut Ausweichmanöver fliegen. Ausweich-Radius: Legt fest, wie nah gegnerisches Kanonenfeuer kommen muss, bevor die KI Ausweichmanöver einleitet. Wird üblicherweise auf die halbe Spannweite des Flugzeugs gesetzt. Ausweich-Verzögerung: Zeitdauer, für die das Flugzeug durchgehend bedroht sein muss, bevor die KI Ausweichmanöver einleitet. Meinem Ziel nicht ausweichen: Keine Ausweichmanöver, wenn die Bedrohung von dem Flugzeug ausgeht, welches zu diesem Zeitpunkt das Ziel des eigenen Flugzeugs ist. + #LOC_BDArmory_AIWindow_EvadeHelp_Dodge = Zusammenstoß-Abstand, Zusammenstoß-Vorausberechnung, Ausweich-Stärke: Diese Einstellung bestimmen, wie die KI auf mögliche Kollisionen mit anderen Flugzeugen reagiert. Zusammenstoß-Abstand bestimmt den Abstand, ab dem von einer Kollision auszugehen ist. Sollte auf die halbe Spannweite des eigenen plus die durchschnittliche Spannweite aller gegnerischen Flugzeuge gesetzt werden. Zusammenstoß-Vorausberechnung legt fest, wie viele Sekunden die aktuellen Trajektorien der beteiligten Flugzeuge in die Zukunft extrapoliert werden, um den möglichen Zusammenstoß (Abstand wird unterschritten) vorherzusehen. Ein großer Wert ermöglicht Flugzeugen, die nicht sehr wendig sind, ein rechtzeitiges Ausweichen. Andererseits kann sich die Trajektorie gerade bei klenien, wendigen Flugzeugen schnell ändern. Hier sind kleinere Werte von Vorteil. + #LOC_BDArmory_AIWindow_EvadeHelp_standoff = Sicherheitsabstand - Bestimmt den Mindestabstand, unterhalb dessen die KI den Angriff abbrechen und Abstand gewinnen wird. + #LOC_BDArmory_AIWindow_EvadeHelp_Extend = Abstand Gewinnen - Abstands-Faktor. Wenn die Funktion Abstand Gewinnen aktiv ist, entspricht ein Abstands-Faktor von 1 der Standard-Distanz von 300m. + #LOC_BDArmory_AIWindow_EvadeHelp_ExtendAbortTime = Abstand gewinnen abbrechen nach x Sekunden - Die KI wird nach x Sekunden aufhören zu versuchen, Abstand zu gewinnen. Nach Abbruch wird für 5 Sekunden kein erneutes Abstand gewonnen gestartet. + #LOC_BDArmory_AIWindow_EvadeHelp_ExtendVars = Winkel zum Ziel, Ziel-Abstand, Abstand Gewinnen Geschwindigkeits-Faktor - Diese drei Einstellungen bestimmen, unter welchen Umständen die KI einen Angriff abbrechen und Abstand gewinnen wird. Abstand gewinnen dient grundsätzlich dazu, das Flugzeug in eine gute Schussposition zu bringen. Einen Angriff vorübergehend abzubrechen kann sinnvoll sein, wenn das Ziel zu nah oder zu weit seitlich positioniert ist, oder wenn es so langsam ist, dass das eigene Flugzeug (vor einem versehentlichen Vorbeiflug) nich in Schussposition kommen kann. + #LOC_BDArmory_AIWindow_EvadeHelp_ExtendAngle = Winkel zum Ziel - Bestimmt den Winkel zum Ziel, bei dessen Überschreiten die KI Abstand gewinnen wird. + #LOC_BDArmory_AIWindow_EvadeHelp_ExtendDist = Ziel-Abstand - Bestimmt den Abstand, bei dessen Unterschreiten die KI versuchen wird, Abstand zu gewinnen. + #LOC_BDArmory_AIWindow_EvadeHelp_ExtendVel = Abstands-Geschwindigkeits-Faktor: Relativer Geschwindigkeitsunterschied zwischen Ziel und eigenem Flugzeug. Ein Wert unter 1 bedeutet, dass das Ziel langsamer fliegt als das eigene Flugzeug. Ein Wert von 0.7 bedeutet, dass die KI Abstand gewinnen wird, wenn das gegnerische Flugzeug mit weniger als 70% der Geschwindigkeit des eigenen Flugzeugs fliegt - in diesem Fall kann es passieren, dass das Ziel überholt wird. + #LOC_BDArmory_AIWindow_EvadeHelp_Nonlinearity = Haken-Schlagen - Dieser Parameter bestimmt in der Form eines Abweichungswinkels, wie große Oszillationen die KI um die mittlere Flugrichtung herum fliegen wird, um während des Abstand Gewinnens oder Ausweichens ein schwerer zu treffendes Ziel abzugeben. + #LOC_BDArmory_AIWindow_EvadeHelp_ExtendToggle = Abstand Gewinnen Ein/Aus - Legt fest, ob die Funktion zum Abstand-Gewinnen beim Luftkampf aktiv sein soll. (Bei Bodenangriffen wird weiterhin Abstand Gewonnen, um in eine gute Schussposition zu kommen). + + #LOC_BDArmory_AIWindow_TerrainHelp = Terrainvermeidung ist eine Funktion, die aufgrund von Lage, Geschwindigkeit und Informationen zum Wendekreis des Flugzeug entscheidet, wann das Flugzeug in Steigflug übergehen soll. Bei einem Wert von 1 geht die KI davon aus, dass das Flugzeug bei der Terrainvermeidung einen ideal kleinen Wendekreis fliegen kann. Der Minimalwert wird angewandt, wenn das Flugzeug keinen Neigungswinkel fliegt und zur Terrainvermeidung sofort hochziehen kann. Der Maximalwert wird angewandt, wenn das Flugzeug maximal schlecht positioniert wird, um das Terrain zu vermeiden, nämlich wenn es auf dem Kopf fliegt. In diesem Fall muss ein effektiv größerer Radius angenommen werden, weil das Flugzeug vor dem Hochziehen noch rollen und gieren muss. Der Kritische Winkel für Terrainvermeidung im Rückenflug bestimmt, bis zu welchem horizontalen Lagewinkel das Flugzeug erst rollt/dann hochzieht bzw. sofort im Rückenflug hochzieht. Wegpunkt-Terrainvermeidung beeinflusst Entfernung und Stärke der Änderung der Flugrichtung, wenn zwischen Gelände zwischen dem Flugzeug und dem Wegpunkt ausragt. + #LOC_BDArmory_AIWindow_RamHelp = Die Funtion "Rammen" kontrolliert, ob das Flugzeug aktiv versuchen soll gegnerische Ziele zu rammen. Sie kommt erst dann zum Einsatz, wenn das Flugzeug alle Minution oder alle Waffen verloren hat. Der Parameter Steuerverzögerung wird benötigt, damit die KI die Geschwindigkeit der Ruder und Klappen bei der Planung der Ramm-Trajektorie einbeziehen kann. Je nach Art der Ruder und Klappen, und je nach benötigter Auslenkung muss dieser Wert unterschiedlich hoch gewählt werden. + + #LOC_BDArmory_AIWindow_miscHelp = Misc Settings - These control non-combat behaviors that don't fit elsewhere. + #LOC_BDArmory_AIWindow_orbitHelp = Orbit Direction - This is the direction the AI will orbit when idling, either Clockwise or Counterclockwise. + #LOC_BDArmory_AIWindow_standbyHelp = Standby Toggle - If enabled, the AI will automatically turn on when targets enter its Guard Range. + + // Idle Behavior + #LOC_BDArmory_Orbit = Richtung beim Kreisen\u0020 + #LOC_BDArmory_Orbit_Starboard = Uhrzeigersinn + #LOC_BDArmory_Orbit_Port = Gegen den Uhrzeigersinn + #LOC_BDArmory_Orbit_Random = Zufall + #LOC_BDArmory_StandbyMode = Bereitschaftsmodus + + #LOC_BDArmory_On = An + #LOC_BDArmory_Off = Aus + + #LOC_BDArmory_VehicleType = Art der Kampfeinheit + #LOC_BDArmory_MaxSlopeAngle = Maximaler Steigungswinkel + #LOC_BDArmory_CruiseSpeed = Reisegeschwindigkeit + #LOC_BDArmory_CombatSpeed = Kampfgeschwindigkeit + #LOC_BDArmory_CombatAltitude = Kampf-Höhe + #LOC_BDArmory_TargetPitch = Rückwärtsneigung + #LOC_BDArmory_MaxPitchAngle = Max. Rückwärtsneigung + #LOC_BDArmory_BankAngle = Seitlich Neigung + #LOC_BDArmory_MaxBankAngle = Max. seitliche Neigung + #LOC_BDArmory_BroadsideAttack = Angriffsvektor + #LOC_BDArmory_BroadsideAttack_enabledText = Breitseite + #LOC_BDArmory_BroadsideAttack_disabledText = Bug + #LOC_BDArmory_MinEngagementRange = Min. Angriffsabstand + #LOC_BDArmory_MaxEngagementRange = Max. Angriffsabstand + #LOC_BDArmory_ManeuverRCS = RFS Aktiv + #LOC_BDArmory_ManeuverRCS_enabledText = Manöver + #LOC_BDArmory_ManeuverRCS_disabledText = Kampf + #LOC_BDArmory_MinObstacleMass = Min. Hindernis-Masse + #LOC_BDArmory_PreferredBroadsideDirection = Bevorzugte Breitseite + #LOC_BDArmory_GoesUp = Skala geht bis + #LOC_BDArmory_GoesUp_enabledText = Elf + #LOC_BDArmory_GoesUp_disabledText = Zehn + + //Surface AI GUI context labels + #LOC_BDArmory_DriverAI_VeeType = Die Kampfeinheit operiert auf diesen Terrains + #LOC_BDArmory_DriverAI_SlopeAngle = Maximaler Steigungswinkel des Fahrzeugs + #LOC_BDArmory_DriverAI_MaxSpeed = Maximale Kampfgeschwindigkeit + #LOC_BDArmory_DriverAI_MaxDrift = Maximaler Drift-Winkel bei Kurvenfahrt + #LOC_BDArmory_DriverAI_Pitch = Bevorzugte Rückwärtsneigung + #LOC_BDArmory_DriverAI_SteerMult = Stärke der Kontrolleingabe (Faktor) + #LOC_BDArmory_DriverAI_AtkVector = Orientierung der Kampfeinheit zum Ziel beim Angriff + #LOC_BDArmory_DriverAI_MinEngage = Minimaler Abstand, unter dem die KI nicht angreifen wird + #LOC_BDArmory_DriverAI_MaxEngage = Maximaler Abstand, oberhalb dessen die KI nicht angreifen wird + #LOC_BDArmory_DriverAI_RCS = RFS benutzen bei + #LOC_BDArmory_DriverAI_Mass = Minimale Masse, ab der ein Hindernis umfahren wird + #LOC_BDArmory_DriverAI_BradsideDir = Bevorzugte Seite, die die Kampfeinheit zum Gegner richten soll + + //Surface AI GUI infolink + #LOC_BDArmory_DriverAI_Help = Art der Kampfeinheit - Dieser Parameter teilt der KI mit, ob es sich bei der Kampfeinheit um ein Fahrzeug, ein Schiff, ein Amphibienfahrzeug, oder um einen stationären Geschützturm handelt. + #LOC_BDArmory_DriverAI_Slopes = Maximaler Steigungswinkel und Rückwärtsneigung - Bestimmen den maximalen Steigungswinkel, den die KI fahren darf, und die bevorzugte Rückwärtsneigung, die die KI beibehalten soll. + #LOC_BDArmory_DriverAI_Speeds = Reisegeschwindigkeit und (Kampf-) Höchstgeschwindigkeit bestimmen die bevorzugte Geschwindigkeit, die die KI un Ruhe bzw, beim Kampf anstreben soll. + #LOC_BDArmory_DriverAI_Drift = Maximaler Driftwinkel - Bestimmt den maximalen Driftwinkel bei Kurvenfahrt. + #LOC_BDArmory_DriverAI_bank = Neigungswinkel - Bestimmt den maximalen seitlichen Neigungswinkel, den die KI erlauben soll. + #LOC_BDArmory_DriverAI_steerMult = Stärke der Kontrolleingabe (Faktor) - Entspricht der Stärke PID Regler (Proportional Integral Derivative, Stärke Korrektur Dämpfung) der Oberflächen-KI. Wird dieser Parameter zu niedrig gewählt, dann wird der volle Bewegungsbereich der Steuerelemente nicht ausgenutzt. Zu hoch, und die KI übersteuert. + #LOC_BDArmory_DriverAI_SteerDamp = Entspricht der Dämpfung im PID Regler der Oberflächen-KI. Bestimmt, wie stark die Drehung auf ein Ziel zu vor dem Erreichen des Zielwinkels gebremst wird, um Überschießen zu vermeiden. Bei einem zu niedrigen Wert oszilliert das Fahrzeug um den angestrebten Zielwinkel herum. + #LOC_BDArmory_DriverAI_Orientation = Angriffsvektor, Bevorzugte Breitseite - Bestimmen, wie die KI die Kampfeinheit bei einem Angriff auf das Ziel ausrichten wird: Entweder mit dem Bug zum Ziel, oder mit der Seite (Breitseite). Eine bevorzugte Seite kann für den Breitseiten-Angriff definiert werden. + #LOC_BDArmory_DriverAI_Engagement = Min./Max. Angriffsabstand - Bestimmt den minimalen und maximalen Abstand zum Ziel. Die KI versucht, die Kampfeinheit innerhalb dieses Abstandfensters zu halten. + #LOC_BDArmory_DriverAI_RCS = RFS - Bestimmt, ob die KI das RFS System beim Manövrieren benutzen soll. + #LOC_BDArmory_DriverAI_Mass = Min. Hindernis-Masse - Bestimmt die minimale Basse, ab der ein Hindernis (oder Wrack) umfahren wird, anstatt es zu rammen oder zu überfahren. + + #LOC_BDArmory_DeployAltitude = Abwurf-Höhe + #LOC_BDArmory_EngageRangeMin = Min. Abstand + #LOC_BDArmory_EngageRangeMax = Max. Abstand + #LOC_BDArmory_EngageAir = Gegen Luftziele anwenden + #LOC_BDArmory_EngageMissile = Gegen Raketen anwenden + #LOC_BDArmory_EngageSurface = Gegen Bodenziele anwenden + #LOC_BDArmory_EngageSLW = Gegen Torpedos anwenden + #LOC_BDArmory_DisableEngageOptions = Angriffsziele nicht auswählen + #LOC_BDArmory_EnableEngageOptions = Angriffsziele auswählen + #LOC_BDArmory_MaxStaticLaunchRange = Max. statischer Startabstand + #LOC_BDArmory_MinStaticLaunchRange = Min. statischer Startabstand + #LOC_BDArmory_MaxOffBoresight = Max. Winkelabweichung zum Ziel + #LOC_BDArmory_DetonationDistanceOverride = Benutzerdef. Explosionsabstand + #LOC_BDArmory_DetonateAtMinimumDistance = Bei Minimaldistanz explodieren + #LOC_BDArmory_ProximityTriggerDistance = Gefechtskopf Explosionsdistanz + #LOC_BDArmory_DropTime = Abwurfdauer + #LOC_BDArmory_InCargoBay = In Laderaum:\u0020 + #LOC_BDArmory_InCustomCargoBay = Benutzerdef. Laderaum-Aktion:\u0020 + #LOC_BDArmory_DeployableWeapon = Ausfahrbare Waffe:\u0020 + #LOC_BDArmory_DetonationTime = Explosions-Zeit + #LOC_BDArmory_BallisticOvershootFactor = Ballistischer Überschuss-Faktor + #LOC_BDArmory_BallisticAnglePath = Winkel der Ballistischen Trajektorie + #LOC_BDArmory_CruiseAltitude = Reisehöhe + #LOC_BDArmory_CruisePredictionTime = Verhergesagte Reisedauer + #LOC_BDArmory_GPSTarget = GPS Ziel + #LOC_BDArmory_ChangetoLowAltitudeRange = Abstand für hohe Flughöhe + #LOC_BDArmory_MaxAltitude = Max. Flughöhe + #LOC_BDArmory_TerminalGuidance = Finale Waffenführung:\u0020 + #LOC_BDArmory_Direction = Richtung:\u0020 + #LOC_BDArmory_Direction_disabledText = Lateral + #LOC_BDArmory_Direction_enabledText = Vorwärts + #LOC_BDArmory_DecoupleSpeed = Abkoppelgeschwindigkeit + + #LOC_BDArmory_EMPBlastRadius = EMP Wirkungsradius + #LOC_BDArmory_OrdinanceAvailable = Kampfmittel Vorhanden + #LOC_BDArmory_MissileAssign = Rakete Zuweisen + #LOC_BDArmory_CurrentLocks = Aktuelle Aufschaltungen + + #LOC_BDArmory_SSTank = Selbstdichtender Tank + #LOC_BDArmory_SSTank_On = Selbstdichtenden Tank hinzufügen + #LOC_BDArmory_SSTank_Off = Selbstdichtenden Tank entfernen + #LOC_BDArmory_CASE = Munitions-Schutzstufe + #LOC_BDArmory_FireBottles = Feuerlöscher + #LOC_BDArmory_FB_Remaining = Feuerlöscher übrig + #LOC_BDArmory_FIS = Tank Inertisierung + #LOC_BDArmory_FIS_On = Tank Inertisierung hinzufügen + #LOC_BDArmory_FIS_Off = Tank Inertisierung entfernen + #LOC_BDArmory_Armorcockpit_On = Gepanzertes Cockpit hinzufügen + #LOC_BDArmory_Armorcockpit_Off = Gepanzertes Cockpit entfernen + + #LOC_BDArmory_MaxPitch = Max Schwenkber. (vert.) + #LOC_BDArmory_MinPitch = Min Schwenkber. (vert.) + #LOC_BDArmory_YawRange = Horiz. Schwenkbereich + #LOC_BDArmory_FireLimits = Feuerreichweite + #LOC_BDArmory_FireLimits_disabledText = Keine + #LOC_BDArmory_FireLimits_enabledText = In Reichweite + #LOC_BDArmory_DefaultDetonationRange = Abstand f. ferngezündete Detonation\u0020 + #LOC_BDArmory_ProximityFuzeRadius = Abstand f. Näherungszünder + #LOC_BDArmory_MaxDetonationRange = Maximaler Explosionsradius + #LOC_BDArmory_Barrage = Sperrfeuer + #LOC_BDArmory_ToggleBarrage = Sperrfeuer ein/aus + #LOC_BDArmory_ReturnTurret = Geschützturm zurücksetzen + #LOC_BDArmory_ToggleAnimation = Animation an/aus + #LOC_BDArmory_CustomFireKey = Benutzerdefinierte Feuer-Taste + #LOC_BDArmory_SetCustomFireKey = Benutzerdefinierte Feuer-Taste einstellen + + #LOC_BDArmory_NextTankSetup = Nächstes Tank-Setup + #LOC_BDArmory_PreviousTankSetup = Vorheriges Tank-Setup + #LOC_BDArmory_NextTexture = Nächste Textur + + #LOC_BDArmory_FireMissile = Rakete abfeuern + #LOC_BDArmory_Detonate = Explodieren + #LOC_BDArmory_Resupply = Nachladen + #LOC_BDArmory_GuidanceMode = Führungsmodus + #LOC_BDArmory_Jettison = Auswerfen + #LOC_BDArmory_ToggleTurret = Gefechtsturm ein/aus + #LOC_BDArmory_TurretEnabled = Gefechtsturm aktiv + #LOC_BDArmory_AutoReturn = Automatisch zurücksetzen + #LOC_BDArmory_MissileTurretFireFOV = Schussfeld + #LOC_BDArmory_HideUI = Waffen-Namen Menü ausblenden + #LOC_BDArmory_ShowUI = Waffen-Namen Menü einblenden + #LOC_BDArmory_HideWeaponGroupUI = Waffengruppen-Menü ausblenden + #LOC_BDArmory_SetWeaponGroupUI = Waffengruppen-Menü einblenden + #LOC_BDArmory_Fire = Feuer + #LOC_BDArmory_ToggleRadar = Radar ein/aus + #LOC_BDArmory_ToggleIRST = IRST ein/aus + + #LOC_BDArmory_GuardMode = Wächter-Modus:\u0020 + #LOC_BDArmory_Team = Team + #LOC_BDArmory_Weapon = Waffe + #LOC_BDArmory_FiringInterval = Feuerintervall + #LOC_BDArmory_FiringBurstLength = Feuerstoß Dauer + #LOC_BDArmory_FiringBurstCount = Feuerstoß Anz. Schüsse + #LOC_BDArmory_FiringTolerance = Feuer(fehl)winkel + #LOC_BDArmory_FieldOfView = Sichtfeld + #LOC_BDArmory_VisualRange = Sichtbereich + #LOC_BDArmory_GunsRange = Waffenreichweite + #LOC_BDArmory_MissilesORTarget = Raketen pro Ziel + #LOC_BDArmory_FireAngleOverride_Enable = Feuerwinkel für diese Waffe einstellen + #LOC_BDArmory_FireAngleOverride_Disable = Globalen Feuerwinkel verwenden + #LOC_BDArmory_BurstLengthOverride_Enable = Feuerstoß-Dauer für diese Waffe einstellen + #LOC_BDArmory_BurstLengthOverride_Disable = Globale Feuerstoß-Dauer verwenden + #LOC_BDArmory_FiringAngle = Feuerwinkel + + #LOC_BDArmory_false = Falsch + #LOC_BDArmory_true = Wahr + + #LOC_BDArmory_AddedCost = Zusätzliche Kosten + #LOC_BDArmory_AddedMass = Gewicht des Sicherheitssystems + #LOC_BDArmory_DryMass = Leergewicht + #LOC_BDArmory_Enabled = Aktiviert + #LOC_BDArmory_Disabled = Deaktiviert + #LOC_BDArmory_Enable = aktivieren + #LOC_BDArmory_Disable = deaktivieren + + #LOC_BDArmory_Status = Status + #LOC_BDArmory_Toggle = ein/aus + #LOC_BDArmory_ShowGroupEditor = Gruppen-Editor anzeigen + #LOC_BDArmory_ShowGroupEditor_enabledText = Gruppen-Editor schließen + #LOC_BDArmory_ShowGroupEditor_disabledText = Gruppen-Editor anzeigen + #LOC_BDArmory_DeactivationDepth = Deaktivations-Tiefe + #LOC_BDArmory_Hitpoints = Lebenspunkte + #LOC_BDArmory_FireCountermeasure = Feuer-Gegenmaßnahmen + + #LOC_BDArmory_TogglePilot = Autopilot ein/aus + #LOC_BDArmory_DeactivatePilot = Autopilot ausschalten + #LOC_BDArmory_ActivatePilot = Autopilot einschalten + + #LOC_BDArmory_SelectTeam = Team auswählen + #LOC_BDArmory_OpenGUI = Fenster öffnen + + #LOC_BDArmory_StoreSettings = Einstellungen speichern + #LOC_BDArmory_RestoreSettings = Einstellungen wiederherstellen + #LOC_BDArmory_ControlSurfaceSettings = Ruder/Klappen-Einstellungen + #LOC_BDArmory_StoreControlSurfaceSettings = Ruder/Klappen-Einstellungen speichern + #LOC_BDArmory_RestoreControlSurfaceSettings = Ruder/Klappen-Einstellungen wiederherstellen + + //ammo switch + #LOC_BDArmory_Ammo_Type = Munitionstyp + #LOC_BDArmory_Ammo_LoadedAmmo = Munition + #LOC_BDArmory_Ammo_Slug = Geschoss + #LOC_BDArmory_Ammo_Shot = Streubombe + #LOC_BDArmory_Ammo_AP = Panzerbrechend + #LOC_BDArmory_Ammo_SAP = Semi-Panzerbrechend + #LOC_BDArmory_Ammo_Flak = Näherung + #LOC_BDArmory_Ammo_Explosive = Explosiv + #LOC_BDArmory_Ammo_HE = Hohh-Explosiv + #LOC_BDArmory_Ammo_Shaped = Hohlladung + #LOC_BDArmory_Ammo_Kinetic = Kinetisch + #LOC_BDArmory_Ammo_EMP = E.M.P. + #LOC_BDArmory_Ammo_Choker = Ersticker + #LOC_BDArmory_Ammo_Impulse = Impuls + #LOC_BDArmory_Ammo_Gravitic = Gravitisch + #LOC_BDArmory_Ammo_Incendiary = Brandgeschoss + #LOC_BDArmory_Ammo_Nuclear = Nuklear + #LOC_BDArmory_Ammo_Beehive = Bienenstock + + //Team Icons + #LOC_BDArmory_Icons_title = BDArmory Team-Symbole + #LOC_BDArmory_Icons_PSA = F4 drücken, um KSP Flugzeugsymbole auszublenden + #LOC_BDArmory_Enable_Icons = Team-Symbole aktivieren + #LOC_BDArmory_Icon_show_self = Eigenes Gefährt markieren + #LOC_BDArmory_Icon_teams = Team-Namen anzeigen + #LOC_BDArmory_Icon_names = Flugzeug-Namen anzeigen + #LOC_BDArmory_Icon_score = Bewertung anzeigen + #LOC_BDArmory_Icon_healthbars = Lebensbalken anzeigen + #LOC_BDArmory_Icon_missiles = Raketen-Symbole + #LOC_BDArmory_Icon_debris = Trümmer-Symbole + #LOC_BDArmory_Icon_persist = Nicht mit KSP-UI verbergen + #LOC_BDArmory_Icon_threats = Bedrohungs-Indikator anzeigen + #LOC_BDArmory_Icon_pointers = Zeiger auf Symbole außerhalb d. Bildsch. + #LOC_BDArmory_Icon_scale = Symbol-Größe + #LOC_BDArmory_Icon_distance_threshold = Abstand-Schwellenwert: + #LOC_BDArmory_Icon_telemetry = Telemetrie aktivieren; + #LOC_BDArmory_Icon_colorget = Anwenden + + //Armor stuff + #LOC_BDArmory_ArmorWidth = Breite + #LOC_BDArmory_ArmorWidthR = Breite d. rechten Seite + #LOC_BDArmory_ArmorWidthL = Breite d. linken Seite + #LOC_BDArmory_ArmorLength = Länge + #LOC_BDArmory_ArmorAdjustParts = Anhängende Teile mit verschieben + #LOC_BDArmory_ArmorTriIso = Dreiecks-Typ: Gleichschenklig + #LOC_BDArmory_ArmorTriSca = Triangle Type: Unregelmäßig + #LOC_BDArmory_Wood = Holz + #LOC_BDArmory_Aluminium = Aluminium + #LOC_BDArmory_Steel = Stahl + #LOC_BDArmory_Titanium = Titan + #LOC_BDArmory_Composites = Komposit + #LOC_BDArmory_Armor_HullType = Hüllenmaterial + #LOC_BDArmory_ArmorThickness = Dicke der Panzerung + #LOC_BDArmory_EquivalentThickness = Dicke (Stahl-äquivalent) + #LOC_BDArmory_ArmorRemaining = Integrität d. Panzerung + #LOC_BDArmory_ArmorTotalMass = Gesammtmasse d. Panzerung + #LOC_BDArmory_ArmorTotalCost = Gesammtkosten d. Panzerung + #LOC_BDArmory_ArmorTotalLift = Gesammtauftrieb des Flugzeugs + #LOC_BDArmory_ArmorWingLoading = Auftrieb pro Tonne + #LOC_BDArmory_ArmorStats = Panzerung Eigenschaften + #LOC_BDArmory_ArmorLiftStacking = Auftrieb Stapelung + #LOC_BDArmory_ArmorStrength = Stärke + #LOC_BDArmory_ArmorHardness = Härte + #LOC_BDArmory_ArmorDuctility = Duktilität + #LOC_BDArmory_ArmorDiffusivity = Viskosität + #LOC_BDArmory_ArmorMaxTemp = Sichere Temperatur + #LOC_BDArmory_ArmorDensity = Dichte + #LOC_BDArmory_ArmorMass = Gewicht + #LOC_BDArmory_ArmorCost = Kosten + + #LOC_BDArmory_ArmorCurrent = Aktuelle Panzerung + #LOC_BDArmory_ArmorVisualizer = Visualisierung der Panzerung + #LOC_BDArmory_ArmorHPVisualizer = Visualisierung der Lebenspunkte + #LOC_BDArmory_ArmorHullVisualizer = Visualisierung des Hüllenmaterials + #LOC_BDArmory_ArmorLiftVisualizer = Visualisierung des Auftriebs + #LOC_BDArmory_ArmorSelect = Hüllenmaterial auswählen + #LOC_BDArmory_ArmorTool = BDA Hilfsprogramme + #LOC_BDArmory_Armor_HullMat = Aktuelles Hüllenmaterial + #LOC_BDArmory_Armor_ArmorType = Typ der Panzerung + #LOC_BDArmory_Armor_Hullmass = Masse mit Panzerung + #LOC_BDArmory_BulletResist = Kinetische Widerstandfähigkeit + #LOC_BDArmory_ExplosionResist = Explosions-Widerstandfähigkeit + #LOC_BDArmory_LaserResist = Laser-Widerstandfähigkeit + #LOC_BDArmory_ArmorShatterWarning = Durchschlagende Munition zerstört die Panzerung + + //Space hack stuff + #LOC_BDArmory_Settings_SpaceHacks = Raumkampf-Werkzeuge + #LOC_BDArmory_Settings_SpaceFriction = Reibung + #LOC_BDArmory_Settings_IgnoreGravity = Anziehungskraft ignorieren + #LOC_BDArmory_Settings_Repulsor = Repulsor-Effekt aktivieren + #LOC_BDArmory_Settings_SpaceFrictionMult = Kurvenflug-Faktor + + //Mutator Gamemode stuff + #LOC_BDArmory_Settings_Mutators = Mutatoren + #LOC_BDArmory_MutatorSelect = Mutatoren auswählen + #LOC_BDArmory_Settings_MutatorGlobal = Global anwenden + #LOC_BDArmory_Settings_MutatorKill = Bei Abschuss anwenden + #LOC_BDArmory_Settings_MutatorTimed = Nach Zeit anwenden + #LOC_BDArmory_Settings_MutatorDuration = Zeitdauer + #LOC_BDArmory_UI_MutatorStart = Globalen Mutatoren aktivieren + #LOC_BDArmory_UI_MutatorShuffle = Zufällige Mutatoren! + #LOC_BDArmory_Settings_MutatorNum = Zahl aktiver Mutatoren + #LOC_BDArmory_Settings_MutatorIcons = Mutator-Symbole anzeigen + + //Missile & CM Settings + #LOC_BDArmory_Settings_MissileCMToggle = Einstellungen für Raketen und Gegenmassnahmen anzeigen + #LOC_BDArmory_Settings_AspectedIRSeekers = Infrarot-Verdeckung stört Wärmesuchende Raketen + #LOC_BDArmory_Settings_FlareFactor = Multiplikator für maximale Zahl von Leuchtkörpern anhängig von Hitze des Flugzeugs + #LOC_BDArmory_Settings_ChaffFactor = Multiplikator für Positionsstörung durch Düppel + #LOC_BDArmory_Settings_SmokeDeflectionFactor = Multiplikator für Positionsstörung durch Rauch + #LOC_BDArmory_Settings_APSThreshold = Minimales Kaliber, auf das das Schutzsystem reagiert + } +} \ No newline at end of file diff --git a/BDArmory/Distribution/GameData/BDArmory/Localization/UI/en-us.cfg b/BDArmory/Distribution/GameData/BDArmory/Localization/UI/en-us.cfg index 237aeac93..5856e4e42 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Localization/UI/en-us.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Localization/UI/en-us.cfg @@ -2,10 +2,17 @@ Localization { en-us { + #LOC_BDArmory_Generic_OK = OK #LOC_BDArmory_Generic_Cancel = Cancel #LOC_BDArmory_Generic_New = New #LOC_BDArmory_Generic_On = On #LOC_BDArmory_Generic_Off = Off + #LOC_BDArmory_Generic_Hide = Hide + #LOC_BDArmory_Generic_Show = Show + #LOC_BDArmory_Generic_Load = Load + #LOC_BDArmory_Generic_Save = Save + #LOC_BDArmory_Generic_Help = Help + #LOC_BDArmory_Generic_Select = Select #LOC_BDArmory_VesselStatus_Landed = (Landed) #LOC_BDArmory_VesselStatus_Splashed = (Splashed) #LOC_BDArmory_VesselStatus_Underwater = (Underwater) @@ -19,6 +26,7 @@ Localization #LOC_BDArmory_WMWindow_selectionText = Weapon: <<1>> #LOC_BDArmory_WMWindow_rippleText1 = Barrage: <<1>> RPM #LOC_BDArmory_WMWindow_rippleText2 = Salvo + #LOC_BDArmory_WMWindow_barrageStagger = Stagger #LOC_BDArmory_WMWindow_rippleText3 = Ripple: <<1>> RPM #LOC_BDArmory_WMWindow_rippleText4 = Ripple: OFF #LOC_BDArmory_WMWindow_ListWeapons = Weapons @@ -34,6 +42,7 @@ Localization #LOC_BDArmory_WMWindow_VisualRange = Visual Range #LOC_BDArmory_WMWindow_GunsRange = Guns Range #LOC_BDArmory_WMWindow_MultiTargetNum = Max Turret Tgts + #LOC_BDArmory_WMWindow_MultiMissileNum = Max Missile Tgts #LOC_BDArmory_WMWindow_MissilesTgt = Missiles/Tgt #LOC_BDArmory_WMWindow_TargetType = Target Type: @@ -59,12 +68,14 @@ Localization #LOC_BDArmory_WMWindow_VisualRange_desc = Visual Range - This is how far the AI can see. Targets that are closer than this value will be seen, and the AI will move in to engage. Targets outside this value will require radar to spot. #LOC_BDArmory_WMWindow_GunsRange_desc = Guns Range - This sets the max weapon range for all guns, rockets, or lasers on the vessel. By default, this is set to the longest range non-missile weapon mounted on the craft. The AI will not attempt to fire on vessels outside this range. #LOC_BDArmory_WMWindow_MultiTargetNum_desc = Max Turret Tgts - For vessels that have multiple turrets, this sets how many targets the turrets may independently target and engage, allowing the vessel to engage multiple targets simultaneously. + #LOC_BDArmory_WMWindow_MultiMissileTgtNum_desc = Max Missile Tgts - For vessels with multiple missiles, this sets how many different targets the AI will seek to engage with missiles, switching to a new target once the allowed number of missiles have been launched at the current target. #LOC_BDArmory_WMWindow_MissilesTgt_desc = Missiles/Tgt - This sets how many missiles the AI will fire a target. Once this number of missiles have been fired, the AI will only fire additional missiles once a previously fired missile hits or is otherwise destroyed. #LOC_BDArmory_WMWindow_TargetType_desc = Advanced Targeting Button - This allows setting custom targeting preferences for the AI, allowing it to specifically target weapons, engines, command pods, heaviest parts or some combination of these instead of targeting Center of Mass. #LOC_BDArmory_WMWindow_EngageType_desc = Engagement Options Button - This is a toggle to quickly set weapon engagment options - Air, Surface, Missile, or SLW - for all weapons on the vessel at once. #LOC_BDArmory_WMWindow_TargetPriority = Tgt. Priority #LOC_BDArmory_WMWindow_targetBias = Target Bias + #LOC_BDArmory_WMWindow_targetPreference = Prefer Air Targets #LOC_BDArmory_WMWindow_targetProximity = Target Dist. #LOC_BDArmory_WMWindow_targetAngletoTarget = Angle to Tgt. #LOC_BDArmory_WMWindow_targetAngleDist = Angle / Dist. @@ -80,6 +91,7 @@ Localization #LOC_BDArmory_WMWindow_Prioritues_Desc = Target Priorities - This tab sets AI targeting behavior. #LOC_BDArmory_WMWindow_targetBias_desc = Target Bias - This sets how much the AI will prefer its current target over a potential new one. The higher the value, the greater the AI's bias towards the current target. + #LOC_BDArmory_WMWindow_targetPreference_desc = Target Engagement Preference - This sets the AI's preferred target type. The lower the value, the greater the AI will prefer to target Ground targes, the higher the value, the greater the preference for Air targets. #LOC_BDArmory_WMWindow_targetProximity_desc = Target Distance - This sets the AI's preferred target distance. Set it higher to prioritize closer targets, lower to prefer more distant ones. #LOC_BDArmory_WMWindow_targetAngletoTarget_desc = Angle to Target - This weights the AI's preference towards targets that are at a closer angle from the vessel's prograde vector. The higher the value, the greater the weighting towards targets directly in front of the craft. #LOC_BDArmory_WMWindow_targetAngleDist_desc = Angle / Distance - This weights the AI's preference towards targets, based on angle of target off craft prograde, divided by target distance. Higher values will prioritize targets infront and close to the craft, and low values the opposite. @@ -87,6 +99,7 @@ Localization #LOC_BDArmory_WMWindow_targetClosingTime_desc = Shorter Closing Time - Higher values will weight target selection towards the target than can be reached most quickly, lower values towards targets that are a greater flight time away. #LOC_BDArmory_WMWindow_targetgunNumber_desc = Weapon Number - This weights targeting preference towards target vessel weapon count. Higher values will prioritize targets with more weapons, lower values will prioritize fewer weapons. #LOC_BDArmory_WMWindow_targetMass_desc = Target Mass - This weights targeting preference towards heavier or lighter vessels. Higher values weight towards greater target mass. + #LOC_BDArmory_WMWindow_targetDmg_desc = Target Damage - This weights targeting preference based on amount of damage target has suffered. Higher values weight towards less remaining target health. #LOC_BDArmory_WMWindow_targetAllies_desc = Less Allies Engaging- This weights targeting preference towards vessels not currently under attack by allies. High values will prioritize unengaged vessels, low values will prioritize vessels engaged by allies. #LOC_BDArmory_WMWindow_targetThreat_desc = Target Threat - High values will weight targeting preference towards vessels currently shooting at this vessel, low values towards ignoring attacking vessel. #LOC_BDArmory_WMWindow_targetVIP_desc = Attack VIP / Defend VIP. These weight targeting preference towards attacking an enemy VIP target, or attacking a target that is engaged with an ally VIP if set to high values, and ignoring these targets if set to low values. @@ -100,22 +113,38 @@ Localization #LOC_BDArmory_Settings_OtherSettingsToggle = Other Settings #LOC_BDArmory_Settings_CompSettingsToggle = Competition Settings #LOC_BDArmory_Settings_GMSettingsToggle = GM Settings - #LOC_BDArmory_Settings_DebugSettingsToggle = Debug Settings + #LOC_BDArmory_Settings_DebugSettingsToggle = Debugging #LOC_BDArmory_Settings_AIToolbarButton = AI Toolbar Button + #LOC_BDArmory_Settings_VMToolbarButton = VM Toolbar Button #LOC_BDArmory_Settings_Instakill = Instakill #LOC_BDArmory_Settings_InfiniteAmmo = Infinite Ammo + #LOC_BDArmory_Settings_InfiniteMissiles = Infinite Ordinance #LOC_BDArmory_Settings_BulletFX = Bullet FX #LOC_BDArmory_Settings_BulletHits = Bullet Hits #LOC_BDArmory_Settings_EjectShells = Eject Shells #LOC_BDArmory_Settings_VesselRelativeBulletChecks = Vessel-Relative Bullet Checks #LOC_BDArmory_Settings_AimAssist = Aim Assist + #LOC_BDArmory_Settings_AimAssistMode_Target = Aim Assist Mode (Target) + #LOC_BDArmory_Settings_AimAssistMode_Aimer = Aim Assist Mode (Aimer) + #LOC_BDArmory_Settings_GUIBackgroundOpacity = GUI Background Opacity #LOC_BDArmory_Settings_DrawAimers = Draw Aimers + #LOC_BDArmory_Settings_DebugTelemetry = On-Screen Telemetry #LOC_BDArmory_Settings_DebugLines = Debug Lines - #LOC_BDArmory_Settings_DebugLabels = Debug Labels - #LOC_BDArmory_Settings_DebugArmor = Debug Armor + #LOC_BDArmory_Settings_DebugAI = AI + #LOC_BDArmory_Settings_DebugArmor = Armor + #LOC_BDArmory_Settings_DebugCompetition = Competition + #LOC_BDArmory_Settings_DebugDamage = Damage + #LOC_BDArmory_Settings_DebugMissiles = Missiles + #LOC_BDArmory_Settings_DebugOther = Other + #LOC_BDArmory_Settings_DebugRadar = Detectors + #LOC_BDArmory_Settings_DebugSpawning = Spawning + #LOC_BDArmory_Settings_DebugWeapons = Weapons + #LOC_BDArmory_Settings_ResetScrollZoom = Reset Scroll-Zoom #LOC_BDArmory_Settings_RemoteFiring = Remote Firing #LOC_BDArmory_Settings_ClearanceCheck = Clearance Check #LOC_BDArmory_Settings_AmmoGauges = Ammo Gauges + #LOC_BDArmory_Settings_GaplessParticleEmitters = Gapless Particle Emitters + #LOC_BDArmory_Settings_FlareSmoke = Flare Smoke #LOC_BDArmory_Settings_ShellCollisions = Shell Collisions #LOC_BDArmory_Settings_BulletHoleDecals = Bullet Hole Decals #LOC_BDArmory_Settings_PerformanceLogging = Performance Logging @@ -130,7 +159,9 @@ Localization #LOC_BDArmory_Settings_waterDrag = Underwater Bullet Drag #LOC_BDArmory_Settings_AutoLoadToKSC = Auto-Load To KSC #LOC_BDArmory_Settings_GenerateCleanSave = Generate Clean Save + #LOC_BDArmory_Settings_AutoDisableUI = Auto-Disable UI #LOC_BDArmory_Settings_AutoResumeTournaments = Auto-Resume Tournaments + #LOC_BDArmory_Settings_AutoQuitAtEndOfTournament = Auto-Quit On Tournament End #LOC_BDArmory_Settings_AutoQuitMemoryUsage = Auto-Quit Memory Threshold #LOC_BDArmory_Settings_CurrentMemoryUsageEstimate = Current Memory Usage Estimate #LOC_BDArmory_Settings_TimeOverride = Time Override @@ -144,9 +175,12 @@ Localization #LOC_BDArmory_Settings_RocketExplosiveDamageMultiplier = Rocket Explosive Multiplier #LOC_BDArmory_Settings_MissileExplosiveDamageMultiplier = Missile Explosive Multiplier #LOC_BDArmory_Settings_ExplosiveBattleDamageMultiplier = B.D. Explosive Multiplier + #LOC_BDArmory_Settings_ArmorExplosivePenetrationResistanceMultiplier = Armor Explosion Resistance Multiplier + #LOC_BDArmory_Settings_BuildingDamageMultiplier = Building Damage Multiplier #LOC_BDArmory_Settings_ImplosiveDamageMultiplier = Implosive Damage Multiplier #LOC_BDArmory_Settings_SecondaryEffectDuration = Special Weapon Effects Duration #LOC_BDArmory_Settings_BallisticTrajectorSimulationMultiplier = Ballistic Traj. Sim. Multiplier + #LOC_BDArmory_Settings_ArmorMassMultiplier = Armor Mass Multiplier #LOC_BDArmory_Settings_DebrisCleanUpDelay = Debris Removal Delay #LOC_BDArmory_Settings_Scoring_HeadShot = Head-Shot Time Limit #LOC_BDArmory_Settings_Scoring_KillSteal = Kill-Steal Time Limit @@ -154,6 +188,8 @@ Localization #LOC_BDArmory_Settings_TerrainAlertFrequency = Terrain Check Frequency #LOC_BDArmory_Settings_CameraSwitchFrequency = Camera Switch Frequency #LOC_BDArmory_Settings_DeathCameraInhibitPeriod = Death Camera Inhibit Period + #LOC_BDArmory_Settings_Max_PWing_HP = HP Scaling Threshold + #LOC_BDArmory_Settings_HP_Clamp = Max HP Limit #LOC_BDArmory_Settings_DisableRamming = Disable Ramming #LOC_BDArmory_Settings_DefaultFFATargeting = Default FFA Targeting #LOC_BDArmory_Settings_TagMode = Tag Mode @@ -168,10 +204,15 @@ Localization #LOC_BDArmory_Settings_DestroyWMWhenNotControlled = Destroy Uncontrolled WMs #LOC_BDArmory_Settings_DisplayCompetitionStatus = Display Competition Status #LOC_BDArmory_Settings_DisplayCompetitionStatusHiddenUI = Show Status With UI Hidden + #LOC_BDArmory_Settings_CameraSwitchIncludeMissiles = Camera Switch: Incl. Missiles + #LOC_BDArmory_Settings_ScrollZoomPrevention = Scroll-Zoom Prevention #LOC_BDArmory_Settings_ResetHP = Reset Max HP of Parts #LOC_BDArmory_Settings_ResetArmor = Reset Armor of parts #LOC_BDArmory_Settings_ResetHull = Reset Material of parts + #LOC_BDArmory_Settings_RestoreKAL = Restore KAL #LOC_BDArmory_Settings_IntakeHack = Hack Intakes + #LOC_BDArmory_Settings_PWingsHack = Pwing Edge Lift + #LOC_BDArmory_Settings_PWingsThickHP = PWing Thickness Based Mass/HP #LOC_BDArmory_Settings_KerbalSafety = Kerbal Safety #LOC_BDArmory_Settings_KerbalSafetyInventory = Kerbal Inventory #LOC_BDArmory_Settings_KerbalSafetyInventory_NoChange = No Change @@ -203,6 +244,7 @@ Localization #LOC_BDArmory_Settings_BD_Leak_Rate = Leak Amount #LOC_BDArmory_Settings_BD_Leak_Time = Leak Duration #LOC_BDArmory_Settings_BD_SubSystems = Subsystem Damage + #LOC_BDArmory_Settings_BD_JointStrength = Structural Damage #LOC_BDArmory_Settings_BD_Ammo = Ammo Explosions #LOC_BDArmory_Settings_BD_Volatile_Ammo = Ammo Bins Explode When Destroyed #LOC_BDArmory_Settings_BD_Ammo_Mult = Explosion Dmg @@ -221,6 +263,7 @@ Localization #LOC_BDArmory_Engines = Engines #LOC_BDArmory_Command = Cockpits #LOC_BDArmory_Mass = Heaviest Parts + #LOC_BDArmory_Random = Random Parts #LOC_BDArmory_Ammo_Setup = Ammo Loadout Configuration #LOC_BDArmory_Ammo_Weapon = Selected Weapon: @@ -231,6 +274,7 @@ Localization #LOC_BDArmory_save = Save #LOC_BDArmory_reset = Reset + // Vessel Spawner #LOC_BDArmory_BDAVesselSpawner_Title = BDA Vessel Spawner #LOC_BDArmory_Settings_SpawnOptions = Spawn Options #LOC_BDArmory_Settings_VesselSpawnGeoCoords = Set Vessel Spawn Point Here @@ -244,6 +288,8 @@ Localization #LOC_BDArmory_Settings_SpawnDumpLogsEverySpawn = Dump Logs Every Spawn (CS) #LOC_BDArmory_Settings_SpawnContinueSingleSpawning = Continuous Single Spawn (S) #LOC_BDArmory_Settings_SpawnRandomOrder = Random Spawn Order (S) + #LOC_BDArmory_Settings_SpawnStartCompetitionAutomatically = Start Competition Automatically + #LOC_BDArmory_Settings_SpawnSpawnProbeHere = Spawn Spawn-Probe Here #LOC_BDArmory_Settings_OutOfAmmoKillTime = Out-of-ammo Kill Time (CS) #LOC_BDArmory_Settings_ClearDebrisNow = Clear Debris Now #LOC_BDArmory_Settings_ClearBystandersNow = Clear Bystanders Now @@ -259,6 +305,7 @@ Localization #LOC_BDArmory_Settings_Teams = Teams #LOC_BDArmory_Settings_Teams_FFA = FFA #LOC_BDArmory_Settings_Teams_Folders = Per Folder / Per Craft File + #LOC_BDArmory_Settings_Teams_Custom_Template = Custom Template #LOC_BDArmory_Settings_Teams_SplitEvenly = Split Evenly Into #LOC_BDArmory_Settings_SpawnFilesLocation = Craft Files Location @@ -267,10 +314,41 @@ Localization #LOC_BDArmory_Settings_ContinuousSpawning = Continuous Spawning #LOC_BDArmory_Settings_CancelSpawning = Cancel Spawning + // Vessel Mover + #LOC_BDArmory_VesselMover_Title = BDA Vessel Mover + #LOC_BDArmory_VesselMover_MoveVessel = Move Vessel + #LOC_BDArmory_VesselMover_SpawnVessel = Spawn Vessel + #LOC_BDArmory_VesselMover_RecoverVessel = Recover Vessel + #LOC_BDArmory_VesselMover_ChooseCrew = Choose Crew + #LOC_BDArmory_VesselMover_PlaceAfterSpawn = Place After Spawn + #LOC_BDArmory_VesselMover_PlaceVessel = Place Vessel + #LOC_BDArmory_VesselMover_DropVessel = Drop Vessel + #LOC_BDArmory_VesselMover_InstantLowering = Instant Lowering + #LOC_BDArmory_VesselMover_ClassicChooser = Classic Craft File Chooser + #LOC_BDArmory_VesselMover_EnableBrakes = Enable Brakes + #LOC_BDArmory_VesselMover_EnableSAS = Enable SAS + #LOC_BDArmory_VesselMover_MinLowerSpeed = Min Lower Speed + #LOC_BDArmory_VesselMover_LowerFast = Placement-Lower + #LOC_BDArmory_VesselMover_BelowWater = Below Water + #LOC_BDArmory_VesselMover_DontWorryAboutCollisions = Don't Avoid Collisions + #LOC_BDArmory_VesselMover_Any = Any + #LOC_BDArmory_VesselMover_ReallyRemoveKerbals = REALLY REMOVE KERBALS‽ + #LOC_BDArmory_VesselMover_Help_Movement = Movement + #LOC_BDArmory_VesselMover_Help_Roll = Roll + #LOC_BDArmory_VesselMover_Help_Pitch = Pitch + #LOC_BDArmory_VesselMover_Help_Yaw = Yaw + #LOC_BDArmory_VesselMover_Help_AutoRotateRocket = Auto-Rotate Rocket + #LOC_BDArmory_VesselMover_Help_AutoRotatePlane = Auto-Rotate Plane + #LOC_BDArmory_VesselMover_Help_CycleAltitudes = Cycle Preset Altitudes: Tab, Shift+Tab + #LOC_BDArmory_VesselMover_Help_ResetAltitude = Reset Altitude + #LOC_BDArmory_VesselMover_Help_AdjustAltitude = Adjust Altitude + #LOC_BDArmory_VesselMover_CloseOnCompetitionStart = Close On Competition Start + // Waypoints game mode #LOC_BDArmory_Settings_WaypointsMode = Waypoints Mode #LOC_BDArmory_Settings_WaypointsOptions = Waypoints Options #LOC_BDArmory_Settings_WaypointsOneAtATime = One-At-A-Time + #LOC_BDArmory_Settings_WaypointsInfFuelAtStart = Infinite Propellant Until First Waypoint #LOC_BDArmory_Settings_WaypointsShow = Show Waypoints // Tournaments @@ -282,11 +360,37 @@ Localization #LOC_BDArmory_Settings_TournamentVesselsPerHeat = Vessels Per Heat #LOC_BDArmory_Settings_TournamentVesselsPerTeam = Vessels Per Team Per Heat #LOC_BDArmory_Settings_TournamentTeamsPerHeat = Teams Per Heat + #LOC_BDArmory_Settings_GauntletOpponentsFilesLocation = Gauntlet Opponent Files + #LOC_BDArmory_Settings_TournamentOpponentTeamsPerHeat = Opponent Teams Per Heat + #LOC_BDArmory_Settings_TournamentOpponentVesselsPerTeam = Opponent Vessels Per Team #LOC_BDArmory_Settings_TournamentFullTeams = Re-use Craft To Fill Teams #LOC_BDArmory_Settings_TournamentSetup = Set Up Tournament #LOC_BDArmory_Settings_TournamentRun = Run Tournament #LOC_BDArmory_Settings_TournamentStop = Stop Tournament + // Custom Spawn Templates + #LOC_BDArmory_Settings_CustomSpawnTemplateOptions = Spawn Template Options + #LOC_BDArmory_Settings_SpawnOnly = Spawn Only + #LOC_BDArmory_Settings_SpawnAndStartCompetition = Spawn and Start Competition + #LOC_BDArmory_Settings_CustomSpawnTemplate_ReplaceTeam = Replace Teams + #LOC_BDArmory_Settings_CustomSpawnTemplate_InvalidParts = INVALID PARTS + #LOC_BDArmory_Settings_CustomSpawnTemplate_UnknownModules = Modules + #LOC_BDArmory_Settings_CustomSpawnTemplate_Clear = Clear + #LOC_BDArmory_Settings_CustomSpawnTemplate_ClearAll = Clear All + #LOC_BDArmory_Settings_CustomSpawnTemplate_Refresh = Refresh + #LOC_BDArmory_Settings_CustomSpawnTemplate_Parts = Parts + #LOC_BDArmory_Settings_CustomSpawnTemplate_Mass = Mass + #LOC_BDArmory_Settings_CustomSpawnTemplate_Version = Version + #LOC_BDArmory_Settings_CustomSpawnTemplate_TemplateSelection = Template Selection + #LOC_BDArmory_Settings_CustomSpawnTemplate_CrewSelection = Crew Selection + #LOC_BDArmory_Settings_CustomSpawnTemplate_VesselSelection = Vessel Selection + + // Observers + #LOC_BDArmory_Settings_Observers = Observers + #LOC_BDArmory_ObserverSelection_Title = Observer Selection + #LOC_BDArmory_ObserverSelection_SelectAll = Select All + #LOC_BDArmory_ObserverSelection_SelectNone = Select None + #LOC_BDArmory_Settings_HeartBleed = Heart Bleed #LOC_BDArmory_Settings_HeartBleedRate = Heart Bleed Rate #LOC_BDArmory_Settings_HeartBleedInterval = Heart Bleed Interval @@ -313,6 +417,7 @@ Localization #LOC_BDArmory_Settings_RWRWindowScale = RWR Window Scale #LOC_BDArmory_Settings_RadarWindowScale = Radar Window Scale + #LOC_BDArmory_Settings_LogarithmicRWRDisplay = Logarithmic RWR Display #LOC_BDArmory_Settings_TargetWindowScale = Target Window Scale #LOC_BDArmory_Settings_TargetWindowInvertMouse = Invert Mouse (Targeting Window) #LOC_BDArmory_Settings_TriggerHold = Trigger Hold @@ -321,6 +426,8 @@ Localization #LOC_BDArmory_Settings_CompetitionDistance = Competition Distance #LOC_BDArmory_Settings_CompetitionDuration = Competition Duration + #LOC_BDArmory_Settings_CompetitionIntraTeamSeparation = Intra-Team Separation + #LOC_BDArmory_Settings_CompetitionIntraTeamSeparationPerMember = / Member #LOC_BDArmory_Settings_CompetitionFinalGracePeriod = Final Grace Period #LOC_BDArmory_Settings_CompetitionInitialGracePeriod = Initial Grace Period #LOC_BDArmory_Settings_CompetitionKillTimer = Landed Kill Timer @@ -335,11 +442,13 @@ Localization #LOC_BDArmory_Settings_StopCompetition = Stop Competition #LOC_BDArmory_Settings_StartCompetitionNow = Start Competition NOW #LOC_BDArmory_Settings_CompetitionStartNowAfter = Start Comp. NOW Delay + #LOC_BDArmory_Settings_CompetitionStartDespiteFailures = Start Comp. Despite Failures #LOC_BDArmory_Settings_StartRapidDeployment = Start Rapid Deployment #LOC_BDArmory_Settings_StartOrbitalDeployment = Start Orbital Deployment #LOC_BDArmory_Settings_LowGravDeployment = Start Low-Grav Takeoff Competiton #LOC_BDArmory_Settings_EditInputs = Edit Inputs #LOC_BDArmory_Settings_CompetitionCloseSettingsOnCompetitionStart = Close Settings When Starting Competitions + #LOC_BDArmory_Settings_CompetitionWaypointTimeThreshold = Waypoint Time Threshold #LOC_BDArmory_BDARemoteOrchestration_Title = BDA Remote Orchestration #LOC_BDArmory_Settings_RemoteLogging = Remote Orchestration @@ -436,6 +545,16 @@ Localization #LOC_BDArmory_DynamicDampingRollMin = Off-target Roll Damping #LOC_BDArmory_DynamicDampingRollMax = On-target Roll Damping #LOC_BDArmory_DynamicDampingRollFactor = Dyn. Roll Damping Factor + + #LOC_BDArmory_PIDAutoTune = PID Auto-Tune + #LOC_BDArmory_AutoTuningLoss = Auto-Tuning Loss + #LOC_BDArmory_PIDAutoTuningNumSamples = Auto-Tuning Number Of Samples + #LOC_BDArmory_PIDAutoTuningFastResponseRelevance = Auto-Tuning Fast Response Relevance + #LOC_BDArmory_PIDAutoTuningInitialLearningRate = Auto-Tuning Initial Learning Rate + #LOC_BDArmory_PIDAutoTuningSpeed = Auto-Tuning Speed + #LOC_BDArmory_PIDAutoTuningAltitude = Auto-Tuning Altitude + #LOC_BDArmory_PIDAutoTuningFixedP = Auto-Tuning Fixed P + #LOC_BDArmory_PIDAutoTuningClampMaximums = Auto-Tuning Clamp Max #LOC_BDArmory_TargetPriority = Target Priority #LOC_BDArmory_TargetPriority_CurrentTarget = Current Target @@ -443,11 +562,13 @@ Localization #LOC_BDArmory_TargetPriority_Settings = Target Priority Settings #LOC_BDArmory_TargetPriority_CurrentTargetBias = Current Target Bias #LOC_BDArmory_TargetPriority_TargetProximity = Target Distance + #LOC_BDArmory_TargetPriority_AirVsGround = Aerial Target Preference #LOC_BDArmory_TargetPriority_CloserAngleToTarget = Closer Angle to Target #LOC_BDArmory_TargetPriority_TargetAcceleration = Target TWR #LOC_BDArmory_TargetPriority_ShorterClosingTime = Shorter Closing Time #LOC_BDArmory_TargetPriority_TargetWeaponNumber = Target Weapon Number #LOC_BDArmory_TargetPriority_TargetMass = Target Mass + #LOC_BDArmory_TargetPriority_TargetDmg = Target Damage #LOC_BDArmory_TargetPriority_FewerTeammatesEngaging = Less Teammates Engaging #LOC_BDArmory_TargetPriority_TargetThreat = Target Threat #LOC_BDArmory_TargetPriority_AngleOverDistance = Angle / Distance @@ -463,6 +584,7 @@ Localization #LOC_BDArmory_ChaffRepetition = Chaff Dump per Sequence #LOC_BDArmory_ChaffInterval = Chaff Dump Interval Time #LOC_BDArmory_ChaffWaitTime = Chaff Sequence Restart Delay + #LOC_BDArmory_ChaffFactor = Chaff Susceptibility #LOC_BDArmory_IsVIP = Is VIP? #LOC_BDArmory_IsVIP_enabledText = Yes @@ -494,6 +616,7 @@ Localization #LOC_BDArmory_StrafingSpeed = Strafing Speed #LOC_BDArmory_IdleSpeed = Idle Speed #LOC_BDArmory_ABPriority = Afterburner Priority + #LOC_BDArmory_ABOverrideThreshold = Afterburner Override Threshold #LOC_BDArmory_MaxDrift = Max drift @@ -503,22 +626,28 @@ Localization #LOC_BDArmory_LowSpeedLimiterSpeed = Low-Speed Limiter Speed #LOC_BDArmory_HighSpeedSteerLimiter = High-Speed Steer Limiter #LOC_BDArmory_HighSpeedLimiterSpeed = High-Speed Limiter Speed + #LOC_BDArmory_AltitudeSteerLimiterFactor = Altitude Steer Limiter Factor + #LOC_BDArmory_AltitudeSteerLimiterAltitude = Altitude Steer Limiter Altitude #LOC_BDArmory_AttitudeLimiter = Attitude Limit #LOC_BDArmory_BankLimiter = Bank Angle Limit #LOC_BDArmory_WaypointPreRollTime = Waypoint Pre-Roll Time #LOC_BDArmory_WaypointYawAuthorityTime = Waypoint Yaw Authority Time #LOC_BDArmory_maxAllowedGForce = Max G #LOC_BDArmory_maxAllowedAoA = Max AoA + #LOC_BDArmory_postStallAoA = Post-Stall AoA Mode-Switch + #LOC_BDArmory_ImmelmannTurnAngle = Immelmann Turn Angle #LOC_BDArmory_PilotAI_EvadeExtend = Evasion/Extension #LOC_BDArmory_ExtendMultiplier = Extend Multiplier #LOC_BDArmory_ExtendDistanceAirToAir = Extend Distance Air-To-Air + #LOC_BDArmory_ExtendAngleAirToAir = Extend Angle Air-To-Air #LOC_BDArmory_ExtendDistanceAirToGroundGuns = Extend Distance Air-To-Ground (Guns) #LOC_BDArmory_ExtendDistanceAirToGround = Extend Distance Air-To-Ground #LOC_BDArmory_ExtendTargetVel = Extend Target Velocity Factor #LOC_BDArmory_ExtendTargetAngle = Extend Target Angle #LOC_BDArmory_ExtendTargetDist = Extend Target Distance - #LOC_BDArmory_ExtendToggle = Extend Toggle + #LOC_BDArmory_ExtendAbortTime = Extend Abort Time + #LOC_BDArmory_ExtendToggle = Extend Toggle (Air-To-Air) #LOC_BDArmory_MinEvasionTime = Min Evasion Time #LOC_BDArmory_EvasionNonlinearity = Evasion/Extension Nonlinearity #LOC_BDArmory_EvasionThreshold = Evasion Distance Threshold @@ -532,6 +661,9 @@ Localization #LOC_BDArmory_PilotAI_Terrain = Terrain Avoidance #LOC_BDArmory_TurnRadiusTwiddleFactorMin = Terrain Avoidance Tuning Min #LOC_BDArmory_TurnRadiusTwiddleFactorMax = Terrain Avoidance Tuning Max + #LOC_BDArmory_TerrainAvoidanceCriticalAngle = Inverted Terrain Avoidance Critical Angle + #LOC_BDArmory_TerrainAvoidanceVesselReactionTime = Vessel Reaction Time + #LOC_BDArmory_WaypointTerrainAvoidance = Waypoint Terrain Avoidance #LOC_BDArmory_PilotAI_Ramming = Ramming #LOC_BDArmory_ControlSurfaceLag = Ramming Control Surface Lag @@ -559,71 +691,108 @@ Localization #LOC_BDArmory_AIWindow_DynDampMin = Minimum Damping #LOC_BDArmory_AIWindow_DynDampMax = Maximum Damping #LOC_BDArmory_AIWindow_DynDampMult = Damping Magnitude + #LOC_BDArmory_AIWindow_PIDAutoTuningNumSamplesMin = <- Less accurate + #LOC_BDArmory_AIWindow_PIDAutoTuningNumSamplesMax = More accurate -> + #LOC_BDArmory_AIWindow_PIDAutoTuningFastResponseRelevanceMin = <- Better damping + #LOC_BDArmory_AIWindow_PIDAutoTuningFastResponseRelevanceMax = Faster response -> + #LOC_BDArmory_AIWindow_PIDAutoTuningInitialLearningRateContext = Lower this if the changes in the PID values are too large + #LOC_BDArmory_AIWindow_PIDAutoTuningAltitudeContext = Try to stay within ±min altitude of this + #LOC_BDArmory_AIWindow_PIDAutoTuningSpeedContext = Target speed for auto-tuning at #LOC_BDArmory_AIWindow_DefAlt = AI returns to this when idle #LOC_BDArmory_AIWindow_MinAlt = AI tries to remain above this #LOC_BDArmory_AIWindow_MaxAlt = AI tries to remain below this #LOC_BDArmory_AIWindow_maxSpeed = AI will remain below this airspeed - #LOC_BDArmory_AIWindow_takeoff = Speed AI will begin pitch input - #LOC_BDArmory_AIWindow_minSpeed = AI will extend if below this + #LOC_BDArmory_AIWindow_takeoff = Speed AI will begin pitch input on take-off + #LOC_BDArmory_AIWindow_minSpeed = AI will try to regain energy if below this speed #LOC_BDArmory_AIWindow_atkSpeed = Ground attack speed #LOC_BDArmory_AIWindow_idleSpeed = Non-combat cruise speed - #LOC_BDArmory_AIWindow_ABPriority = Afterburner priority + #LOC_BDArmory_AIWindow_ABPriority = Modifies the threshold for enabling the afterburner + #LOC_BDArmory_AIWindow_ABOverrideThreshold_Context = Force use of afterburner if below this speed and full throttle #LOC_BDArmory_AIWindow_LSSL = Limits control below low speed limit - #LOC_BDArmory_AIWindow_LSLS = AI uses Low-Speed Limiter below this - #LOC_BDArmory_AIWindow_HSSL = Limits control above low speed limit - #LOC_BDArmory_AIWindow_HSLS = AI fully limited to high limit above this + #LOC_BDArmory_AIWindow_LSLS = AI uses Low-Speed Limiter below this speed + #LOC_BDArmory_AIWindow_HSSL = Limits control above high speed limit + #LOC_BDArmory_AIWindow_HSLS = AI fully limited to high limit above this speed + #LOC_BDArmory_AIWindow_ASLF = Factor to decrease/increase steer limit based on altitude + #LOC_BDArmory_AIWindow_ASLA = Altitude to begin decreasing/increasing the steer limit #LOC_BDArmory_AIWindow_WPPreRoll = Start rolling ahead of reaching the waypoint #LOC_BDArmory_AIWindow_WPYawAuth = Increase yaw response when approaching waypoint #LOC_BDArmory_AIWindow_bankLimit = Max roll angle - #LOC_BDArmory_AIWindow_GForce = Maneuvers will not exceed this G limit - #LOC_BDArmory_AIWindow_AoA = Maneuver AoA will not exceed this + #LOC_BDArmory_AIWindow_GForce = Maneuvers will try not to exceed this G limit + #LOC_BDArmory_AIWindow_AoA = Maneuver AoA will try not to exceed this AoA + #LOC_BDArmory_AIWindow_AoAPostStall = Switch flight modes for post-stall beyond this AoA + #LOC_BDArmory_AIWindow_ImmelmannTurnAngleContext = Craft will just pitch up to aim at a target in this cone #LOC_BDArmory_AIWindow_MinEvade = Min time AI evades an attack #LOC_BDArmory_AIWindow_EvExNonlin = Strength of oscillations when evading/extending - #LOC_BDArmory_AIWindow_evadeDist = Evades if incoming fire within this + #LOC_BDArmory_AIWindow_evadeDist = Evades if incoming fire within this distance #LOC_BDArmory_AIWindow_evadetimeDist = Min time under fire to trigger evasion #LOC_BDArmory_AIWindow_ColDist = Dodge incoming craft within this dist - #LOC_BDArmory_AIWindow_ColTime = How far ahead AI looks for collisions - #LOC_BDArmory_AIWindow_standoff = Min distance AI will close to on target - #LOC_BDArmory_AIWindow_ExtendMult = Extend distance multiplier + #LOC_BDArmory_AIWindow_ColTime = How many seconds ahead AI looks for collisions + #LOC_BDArmory_AIWindow_ColStrength = How hard the AI will break away from incoming craft + #LOC_BDArmory_AIWindow_standoff = Distance the AI will try to close to on a target + #LOC_BDArmory_AIWindow_ExtendAngleAirToAir_Context = Desired angle of climb when extending #LOC_BDArmory_AIWindow_ExtendDistanceAirToAir_Context = Extend Distance Air-To-Air #LOC_BDArmory_AIWindow_ExtendDistanceAirToGroundGuns_Context = Extend Distance Air-To-Ground (Guns) #LOC_BDArmory_AIWindow_ExtendDistanceAirToGround_Context = Extend Distance Air-To-Ground #LOC_BDArmory_AIWindow_Extendvel = Sets when target is too slow to turn towards #LOC_BDArmory_AIWindow_ExtendAngle = Sets when target is outside turn radius #LOC_BDArmory_AIWindow_ExtendDist = Sets when target is too close to turn towards + #LOC_BDArmory_AIWindow_ExtendAbortTimeContext = Abort time if failing to gain distance while extending. #LOC_BDArmory_AIWindow_terrainMin = Turn radius multiplier for ideal craft orientation #LOC_BDArmory_AIWindow_terrainMax = Turn radius multiplier for inverted craft orientation + #LOC_BDArmory_AIWindow_InvertedTerrainAvoidanceCriticalAngleContext = Critical angle for inverted terrain avoidance or rolling first + #LOC_BDArmory_AIWindow_TerrainAvoidanceVesselReactionTimeContext = Estimate of time required to setup for optimal turning + #LOC_BDArmory_AIWindow_WaypointTerrainAvoidanceContext = Range and strength of waypoint terrain correction #LOC_BDArmory_AIWindow_ramLag = Ram trajectory correction for control surfaces' lag #LOC_BDArmory_AIWindow_orbit = Direction of non-combat cruising #LOC_BDArmory_AIWindow_standby = AI turns on when target enters Guard Range //PilotAi GUI labels + #LOC_BDArmory_AIWindow_PIDAutoTuningNumSamples = A-T Num Samples + #LOC_BDArmory_AIWindow_PIDAutoTuningFastResponseRelevance = A-T Fast Response + #LOC_BDArmory_AIWindow_PIDAutoTuningInitialLearningRate = A-T Initial LR + #LOC_BDArmory_AIWindow_PIDAutoTuningSpeed = A-T Speed + #LOC_BDArmory_AIWindow_PIDAutoTuningAltitude = A-T Altitude + #LOC_BDArmory_AIWindow_PIDAutoTuningFixedP = A-T Fixed P + #LOC_BDArmory_AIWindow_PIDAutoTuningFixedFields = A-T Fixed Fields + #LOC_BDArmory_AIWindow_PIDAutoTuningClampMaximums = A-T Clamp Max #LOC_BDArmory_AIWindow_ControlLimits = Control + #LOC_BDArmory_AIWindow_ABOverrideThreshold = Afterburner Override #LOC_BDArmory_AIWindow_SteerLimiter = Steer Limiter #LOC_BDArmory_AIWindow_LowSpeedSteerLimiter = Low-Speed Limiter #LOC_BDArmory_AIWindow_LowSpeedLimiterSpeed = Low Limiter Speed #LOC_BDArmory_AIWindow_HighSpeedSteerLimiter = High-Speed Limiter #LOC_BDArmory_AIWindow_HighSpeedLimiterSpeed = High Limiter Speed + #LOC_BDArmory_AIWindow_AltitudeSteerLimiterFactor = Alt Steer Factor + #LOC_BDArmory_AIWindow_AltitudeSteerLimiterAltitude = Alt Steer Altitude #LOC_BDArmory_AIWindow_WaypointPreRollTime = WP Pre-Roll Time #LOC_BDArmory_AIWindow_WaypointYawAuthorityTime = WP Yaw Auth Time - #LOC_BDArmory_AIWindow_EvadeExtend = Energy - #LOC_BDArmory_AIWindow_ExtendMultiplier = Extend Mult. - #LOC_BDArmory_AIWindow_ExtendDistanceAirToAir = Ext. Dist. A2A - #LOC_BDArmory_AIWindow_ExtendDistanceAirToGroundGuns = Ext. Dist. A2G Guns - #LOC_BDArmory_AIWindow_ExtendDistanceAirToGround = Ext. Dist. A2G + #LOC_BDArmory_AIWindow_postStallAoA = Post-Stall AoA + #LOC_BDArmory_AIWindow_ImmelmannTurnAngle = Immelmann Turn Angle + #LOC_BDArmory_AIWindow_EvadeExtend = Evade/Extend + #LOC_BDArmory_AIWindow_Evade = Evasion + #LOC_BDArmory_AIWindow_EvasionThreshold = Distance Threshold + #LOC_BDArmory_AIWindow_EvasionTimeThreshold = Time Threshold + #LOC_BDArmory_AIWindow_EvasionNonlinearity = Evade/Extend Nonlinearity + #LOC_BDArmory_AIWindow_Extend = Extension + #LOC_BDArmory_AIWindow_ExtendDistanceAirToAir = Distance A2A + #LOC_BDArmory_AIWindow_ExtendAngleAirToAir = Angle A2A + #LOC_BDArmory_AIWindow_ExtendDistanceAirToGroundGuns = Distance A2G Guns + #LOC_BDArmory_AIWindow_ExtendDistanceAirToGround = Distance A2G #LOC_BDArmory_AIWindow_ExtendTargetVel = Target Vel. Factor #LOC_BDArmory_AIWindow_ExtendTargetAngle = Target Angle - #LOC_BDArmory_AIWindow_ExtendTargetDist = Target Dist. - #LOC_BDArmory_AIWindow_EvasionNonlinearity = Evade/Extend Nonlin - #LOC_BDArmory_AIWindow_EvasionThreshold = Evasion Dist. - #LOC_BDArmory_AIWindow_EvasionTimeThreshold = Evasion Threshold - #LOC_BDArmory_AIWindow_CollisionAvoidanceThreshold = Craft Avoid Range - #LOC_BDArmory_AIWindow_CollisionAvoidanceLookAheadPeriod = Craft Avoid Look-Ahead - #LOC_BDArmory_AIWindow_CollisionAvoidanceStrength = Craft Avoid Strength + #LOC_BDArmory_AIWindow_ExtendTargetDist = Target Distance + #LOC_BDArmory_AIWindow_ExtendAbortTime = Abort time + #LOC_BDArmory_AIWindow_Avoidance = Vessel Avoidance + #LOC_BDArmory_AIWindow_CollisionAvoidanceThreshold = Distance Threshold + #LOC_BDArmory_AIWindow_CollisionAvoidanceLookAheadPeriod = Look-Ahead Time + #LOC_BDArmory_AIWindow_CollisionAvoidanceStrength = Response Strength #LOC_BDArmory_AIWindow_StandoffDistance = Stand-off Dist. #LOC_BDArmory_AIWindow_Terrain = Terrain #LOC_BDArmory_AIWindow_TurnRadiusMin = Terrain Avoid Min #LOC_BDArmory_AIWindow_TurnRadiusMax = Terrain Avoid Max + #LOC_BDArmory_AIWindow_InvertedTerrainAvoidanceCriticalAngle = Inverted Crit. Angle + #LOC_BDArmory_AIWindow_TerrainAvoidanceVesselReactionTime = Vessel Reaction Time + #LOC_BDArmory_AIWindow_WaypointTerrainAvoidance = WP Terrain Avoid #LOC_BDArmory_AIWindow_ControlSurfaceLag = Control srf. Lag //Pilot AI infolink @@ -632,6 +801,8 @@ Localization #LOC_BDArmory_AIWindow_PidHelp_SteerKi = Steer Ki (I) - This is the error correction applied fix accumulated error from P and D. Too little, and the craft will consistently undershoot its desired orientation. Too much, and it will overshoot. #LOC_BDArmory_AIWindow_PidHelp_Steerdamp = Steer Damp (D) - This is the derivative value; this is how much the craft's rotation to a new orientation will be damped out. Too low, and the craft will oscillate due to overshooting the correct orientation. Too high, and the damping will counter too much of the change in orientation, and craft will not turn as fast. #LOC_BDArmory_AIWindow_PidHelp_Dyndamp = Dynamic Damping - This dynamically adjusts Damping, from the min damping value to the max damping value, based on angle to target. The lower the value, the more linear the dynamic damping value as target angle changes, the higher the value, the more damping will be reduced when pointing away from a target, and increased when pointing near a target. This applies to all three control axes. For individual control Axis damping, enable the relevant Pitch/Roll/Yaw Dynamic Damping. + #LOC_BDArmory_AIWindow_PidHelp_AutoTune = PID Auto-Tune - This enables an automated PID tuning mode where the AI will use gradient descent to optimize the plane's ability to turn to a range of headings and stabilize in those directions. + #LOC_BDArmory_AIWindow_PidHelp_AutoTune_details = \n - The loss being minimized is ∫f(x,θ)dθ over the range θ ∈ (30°,120°) (using the midpoint Riemann sum), where f(x,θ) is\n ∫(δp²·(α+t²)/θ² + γ·δr²·(α+t)/100/θ)dt\nfor the current PID values (x) and heading change (θ), where δp is the pointing error, δr is the roll error, α is the fast response relevance and γ is the roll relevance (which is automatically adjusted over time to balance the contribution from the pointing and roll errors).\n - Usage: Once the plane is airborne (and not in combat), enable auto-tuning and set the sliders to the desired values (the defaults are reasonable starting points and can be preset in the SPH; adjusting some of the sliders will restart the auto-tuning), then allow the auto-tuning to run until it stops automatically when the learning rate (LR) decreases to below 1e-3. The PID values will revert to those giving the lowest loss and these will be stored so they can be restored in the SPH.\n - Parameters: Num Samples - The number of heading changes (θ) used in the Riemann sum; higher values will decrease noise in the gradient. Fast Response (α) - How much to weight the early pointing and roll errors. Initial LR - The initial learning rate. Altitude and Speed - The target altitude and speed to use while tuning. Fixed P - Keep P fixed and only tune the other fields. Clamp Max - Keep the tuned values within the limits of the sliders.\n - Recommendations: 1. Set the auto-tuning altitude and speed to those expected to be used in combat. 2. Use 5-10x time-scaling. 3. Avoid mountainous terrain. 4. Tune without dynamic damping first and use the result as the starting point for dynamic damping with all the damping values set to the tuned static damping value and the dynamic damping factors set to 1. 5. Since the PID values are (currently) being optimized for flying to fixed points, the tuned I value may not be optimal for moving targets in combat and a slightly larger I may be desirable. #LOC_BDArmory_AIWindow_AltHelp = Altitude settings control the desired flight envelope of the AI #LOC_BDArmory_AIWindow_AltHelp_Def = Default Altitude - This is the Altitude the AI will seek to return to when not in combat or extending. @@ -644,24 +815,28 @@ Localization #LOC_BDArmory_AIWindow_SpeedHelp_idle = Idle Speed - This sets the non-combat cruise airspeed the AI will maintain while orbiting or flying to position. #LOC_BDArmory_AIWindow_SpeedHelp_gnd = Strafing Speed - This is the airspeed the AI will use when attacking ground targets. If the ground target is moving, Strafing Speed will add the ground target's speed. #LOC_BDArmory_AIWindow_SpeedHelp_ABpriority = AB Priority - This controls the level of requested acceleration at which the AI will turn on/off the afterburners. + #LOC_BDArmory_AIWindow_SpeedHelp_ABOverrideThreshold = AB Override Threshold - Below this speed threshold, the AI will turn on the afterburners if the throttle is at max. #LOC_BDArmory_AIWindow_ControlHelp = Control Limits set limits on craft control authority under various conditions - #LOC_BDArmory_AIWindow_ControlHelp_limiters = Steer Limiters - Steer Limiters limit Control Authority of the craft. The Low Speed Limiter sets AI control authority when at or below the Low-Speed Limit Speed. The High-Speed Limiter sets AI control authority when at or above the High-Speed Limit Speed. When between the Low and High Limit Speeds, the Limiter value linearly changes from the Low Limit to the High Limit value. + #LOC_BDArmory_AIWindow_ControlHelp_limiters = Steer Limiters - Steer Limiters limit Control Authority of the craft. The Low Speed Limiter sets AI control authority when at or below the Low-Speed Limit Speed. The High-Speed Limiter sets AI control authority when at or above the High-Speed Limit Speed. When between the Low and High Limit Speeds, the Limiter value linearly changes from the Low Limit to the High Limit value. The Altitude Steer Limiter scales the steer limit based on altitude above the limit by (altitude/limit)^factor. #LOC_BDArmory_AIWindow_ControlHelp_bank = Max Bank - This sets the max allowed bank angle. When below 180, the AI will not roll past this many degrees from Horizontal during maneuvers. #LOC_BDArmory_AIWindow_ControlHelp_clamps = Max Allowed G and AoA - Max Allowed G limits the AI to maneuvers that pull this many Gs or less. Max Allowed AoA limits the the maximum Angle of Attack the AI can use. + #LOC_BDArmory_AIWindow_ControlHelp_modeSwitches = Post-Stall AoA Mode-Switch controls when the plane will switch steering modes due to being beyond the AoA threshold. When flying to a target within the angle of the Immelmann Turn, the craft will simply pitch up instead of rolling. #LOC_BDArmory_AIWindow_EvadeHelp = Evasion/Extension controls both how the AI reacts to incoming threats, be it gunfire, missiles, or other craft, and how the AI responds to where other craft are, relative to itself. - #LOC_BDArmory_AIWindow_EvadeHelp_Evade = EvasionTime, EvasionThreshold, EvasionTimeThreshold, Don't Evade My Target - These four settings control when the AI will evade. EvasionTime sets how many seconds the AI will do evasive maneuvers. EvasionTimeThreshold sets how long the AI needs to be under fire before it begins evading. EvasionThreshold sets how close incoming gunfire needs to come to trigger evasion. The Don't Evade My Target toggle determines whether gunfire from the current target is ignored for evasion purposes. + #LOC_BDArmory_AIWindow_EvadeHelp_Evade = Min Evasion Time, Distance Threshold, Time Threshold, Don't Evade My Target - These four settings control when the AI will evade. Min Evasion Time sets how many seconds the AI will do evasive maneuvers. Distance Threshold sets how close incoming gunfire needs to come to trigger evasion. Time Threshold sets how long the AI needs to be under fire before it begins evading. The Don't Evade My Target toggle determines whether gunfire from the current target is ignored for evasion purposes. + #LOC_BDArmory_AIWindow_EvadeHelp_Nonlinearity = Evasion/Extension Nonlinearity - This controls the radius of the oscillation (in degrees) around the fly-to direction that the plane will make while evading or extending. This helps planes not fly in a straight line when evading/extending. #LOC_BDArmory_AIWindow_EvadeHelp_Dodge = Vessel Avoidance - These three settings set how the AI reacts to potential collisions. If another vessel is predicted to get within the Avoidance Threshold within the next Look-Ahead period then the AI will try to dodge it. The Avoidance Strength determines how rapidly the AI will try to change direction to avoid a predicted collision. #LOC_BDArmory_AIWindow_EvadeHelp_standoff = Standoff Distance is the closest the AI will approach the targeted craft in combat. If closer than the Standoff Distance, the AI will brake to increase the distance to the target. - #LOC_BDArmory_AIWindow_EvadeHelp_Extend = Extend Multiplier - If Extending is enabled, this adjusts the distance the AI will extend from its default of 300 meters - #LOC_BDArmory_AIWindow_EvadeHelp_ExtendVars = Target Angle, Target Dist, Extend Target Vel - These three settings together control when the AI will extend. To extend, the AI projects a detection cone ahead of it, checks the distance to the target, and the relative velocity. By default, the AI will extend if the target is outside a 78 degree cone ahead of the AI, closer than 400m, and has a slower airspeed. + #LOC_BDArmory_AIWindow_EvadeHelp_Extend = Extend Distances - These settings control how far the AI will extend against various types of targets. The extend angle controls whether the AI should try to gain or lose altitude when extending against air targets. + #LOC_BDArmory_AIWindow_EvadeHelp_ExtendVars = Target Velocity Factor, Target Angle, Target Distance - These three settings together control when the AI will extend. To extend, the AI projects a detection cone ahead of it, checks the distance to the target, and the relative velocity. By default, the AI will extend if the target is outside a 78 degree cone ahead of the AI, closer than 400m, and has a slower airspeed. + #LOC_BDArmory_AIWindow_EvadeHelp_ExtendVel = Extend Target Velocity Factor - This tells the AI at what relative velocity it should consider extending. Less than 1, the target craft must be slower, greater than 1, faster. #LOC_BDArmory_AIWindow_EvadeHelp_ExtendAngle = Extend Target Angle - This sets the width of the detection cone, and can be though of as a combination field of view and effective turn radius. The better the craft's turn radius, the higher this value can be. #LOC_BDArmory_AIWindow_EvadeHelp_ExtendDist = Extend Target Distance - This sets how close the target has to be before the AI will extend to get a better angle on the target. - #LOC_BDArmory_AIWindow_EvadeHelp_ExtendVel = Extend Target Velocity - This tells the AI at what relative velocity it should consider extending. Less than 1, the target craft must be slower, greater than 1, faster. - #LOC_BDArmory_AIWindow_EvadeHelp_Nonlinearity = Evasion/Extension Nonlinearity - This controls the radius of the oscillation (in degrees) around the fly-to direction that the plane will make while evading or extending. This helps plane not fly in a straight line when evading/extending. + #LOC_BDArmory_AIWindow_EvadeHelp_ExtendAbortTime = Extend Abort Time - This tells the AI to abort extending if it hasn't made any gains within this time. Extending then enters a 5s cooldown period. + #LOC_BDArmory_AIWindow_EvadeHelp_ExtendToggle = Extend Toggle - This enables or disables extending against air targets (extending against ground targets is not affected). - #LOC_BDArmory_AIWindow_TerrainHelp = Terrain Avoidance is used to predict collisions with the ground, by scaling the craft's turn radius to generate a terrain collision distance to tell the AI if it needs to pull up. The Terrain Avoid Min value is based on optimum flight conditions, with the plane parallel to the ground and only needs to pitch up. Terrain Avoid Max is based on the worst case scenerio with the plane inverted to the ground, and needs to roll 180 degrees before pitching up. + #LOC_BDArmory_AIWindow_TerrainHelp = Terrain Avoidance is used to predict collisions with the ground, by scaling the craft's turn radius to generate a terrain collision distance to tell the AI if it needs to pull up. The Terrain Avoid Min value is based on optimum flight conditions, with the plane parallel to the ground and only needs to pitch up. Terrain Avoid Max is based on the worst case scenerio with the plane inverted to the ground, and needs to roll 180 degrees before pitching up. The critical angle for inverted terrain avoidance determines the roll angle w.r.t. the terrain normal at which the plane will try to avoid the terrain while inverted or try to roll up first. The vessel reaction time is an estimate of how long it takes on average for the vessel to get into an optimal setup for a tight turn away from terrain. Waypoint Terrain Avoidance affects the range and strength of the adjustment to the fly-to direction due to terrain being between the craft and the current waypoint. #LOC_BDArmory_AIWindow_RamHelp = Ramming controls if the craft should attempt to ram other craft when it is out of ammo or no longer has working weapons. If ramming is not enabled, the AI will instead continue to maneuver, but be unable to engage. Control Surface lag sets how much the AI should correct collision predictions, based on how long control surfaces take to reach full deflection. @@ -761,6 +936,17 @@ Localization #LOC_BDArmory_Direction_disabledText = Lateral #LOC_BDArmory_Direction_enabledText = Forward #LOC_BDArmory_DecoupleSpeed = Decouple Speed + #LOC_BDArmory_LoftMaxAltitude = Loft Max Altitude + #LOC_BDArmory_LoftRangeOverride = Loft Range Override + #LOC_BDArmory_LoftAltitudeAdvMax = Loft Max Altitude Adv. + #LOC_BDArmory_LoftMinAltitude = Loft Min Altitude + #LOC_BDArmory_LoftAngle = Loft Climb Angle + #LOC_BDArmory_LoftTermAngle = Loft Termination Angle + #LOC_BDArmory_LoftRangeFac = Loft Range Factor + #LOC_BDArmory_LoftVelComp = Loft Velocity Compensation + #LOC_BDArmory_LoftVertVelComp = Loft Vertical Velocity Compensation + //#LOC_BDArmory_LoftAltComp = Loft Altitude Compensation + #LOC_BDArmory_terminalHomingRange = Terminal Homing Range #LOC_BDArmory_EMPBlastRadius = EMP Blast Radius #LOC_BDArmory_OrdinanceAvailable = Ordinance Available @@ -793,6 +979,9 @@ Localization #LOC_BDArmory_ReturnTurret = Return Turret #LOC_BDArmory_ToggleAnimation = Toggle Animation + #LOC_BDArmory_CustomFireKey = Custom Fire Key + #LOC_BDArmory_SetCustomFireKey = Set Custom Fire Key + #LOC_BDArmory_NextTankSetup = Next tank setup #LOC_BDArmory_PreviousTankSetup = Previous tank setup #LOC_BDArmory_NextTexture = Next Texture @@ -812,6 +1001,7 @@ Localization #LOC_BDArmory_SetWeaponGroupUI = Set Weapon Group UI #LOC_BDArmory_Fire = Fire #LOC_BDArmory_ToggleRadar = Toggle Radar + #LOC_BDArmory_ToggleIRST = Toggle IRST #LOC_BDArmory_GuardMode = Guard Mode:\u0020 #LOC_BDArmory_Team = Team @@ -859,10 +1049,14 @@ Localization #LOC_BDArmory_StoreSettings = Store Settings #LOC_BDArmory_RestoreSettings = Restore Settings + #LOC_BDArmory_ControlSurfaceSettings = Control Surfaces Settings + #LOC_BDArmory_StoreControlSurfaceSettings = Store Control Surfaces + #LOC_BDArmory_RestoreControlSurfaceSettings = Restore Control Surfaces //ammo switch #LOC_BDArmory_Ammo_Type = Ammo Type #LOC_BDArmory_Ammo_LoadedAmmo = Ammo + #LOC_BDArmory_Ammo_Multiple = Multiple #LOC_BDArmory_Ammo_Slug = Slug #LOC_BDArmory_Ammo_Shot = Cluster #LOC_BDArmory_Ammo_AP = Armor-Piercing @@ -879,6 +1073,7 @@ Localization #LOC_BDArmory_Ammo_Incendiary = Incendiary #LOC_BDArmory_Ammo_Nuclear = Nuclear #LOC_BDArmory_Ammo_Beehive = Beehive + #LOC_BDArmory_Ammo_Multiple = Multiple //Team Icons #LOC_BDArmory_Icons_title = BDArmory Team UI Icons @@ -901,16 +1096,26 @@ Localization //Armor stuff #LOC_BDArmory_ArmorWidth = Width + #LOC_BDArmory_ArmorWidthR = Right Side Width + #LOC_BDArmory_ArmorWidthL = Left Side Width #LOC_BDArmory_ArmorLength = Length + #LOC_BDArmory_ArmorAdjustParts = Translate Attached Parts + #LOC_BDArmory_ArmorTriIso = Triangle Type: Isoceles + #LOC_BDArmory_ArmorTriSca = Triangle Type: Scalene #LOC_BDArmory_Wood = Wood #LOC_BDArmory_Aluminium = Aluminium #LOC_BDArmory_Steel = Steel + #LOC_BDArmory_Titanium = Titanium + #LOC_BDArmory_Composites = Composites #LOC_BDArmory_Armor_HullType = Hull Material #LOC_BDArmory_ArmorThickness = Armor Thickness #LOC_BDArmory_EquivalentThickness = Steel Equivalent Thickness #LOC_BDArmory_ArmorRemaining = Armor Integrity #LOC_BDArmory_ArmorTotalMass = Total Armor Mass for Craft #LOC_BDArmory_ArmorTotalCost = Total Armor Cost for Craft + #LOC_BDArmory_ArmorTotalLift = Total Lift for Craft + #LOC_BDArmory_ArmorWingLoading = Lift-to-Mass Ratio for Craft + #LOC_BDArmory_ArmorLiftStacking = Lift Stacking #LOC_BDArmory_ArmorStats = Armor Properties #LOC_BDArmory_ArmorStrength = Strength #LOC_BDArmory_ArmorHardness = Hardness @@ -924,11 +1129,11 @@ Localization #LOC_BDArmory_ArmorVisualizer = Toggle Armor Visualizer #LOC_BDArmory_ArmorHPVisualizer = Toggle HP Visualizer #LOC_BDArmory_ArmorHullVisualizer = Toggle Hull Visualizer + #LOC_BDArmory_ArmorLiftVisualizer = Toggle Lift Visualizer #LOC_BDArmory_ArmorSelect = Select Armor Material - #LOC_BDArmory_ArmorTool = BDA Armor Tool + #LOC_BDArmory_ArmorTool = BDA Craft Utilities Tool #LOC_BDArmory_Armor_HullMat = Current Hull Material #LOC_BDArmory_Armor_ArmorType = Armor Type - #LOC_BDArmory_Armor_HullType = Hull Type #LOC_BDArmory_Armor_Hullmass = Adjusted Part Mass #LOC_BDArmory_BulletResist = Kinetic Resist. #LOC_BDArmory_ExplosionResist = Explosive Resist. @@ -953,5 +1158,13 @@ Localization #LOC_BDArmory_UI_MutatorShuffle = Mutators Shuffled! #LOC_BDArmory_Settings_MutatorNum = Number of Active Mutators #LOC_BDArmory_Settings_MutatorIcons = Show Mutator Icons + + //Missile & CM Settings + #LOC_BDArmory_Settings_MissileCMToggle = Show Missile & Countermeasure Settings + #LOC_BDArmory_Settings_AspectedIRSeekers = IR Occlusion Affects Missiles + #LOC_BDArmory_Settings_FlareFactor = Max Flare Start Heat Mult. + #LOC_BDArmory_Settings_ChaffFactor = Chaff Pos. Distortion Mult. + #LOC_BDArmory_Settings_SmokeDeflectionFactor = Smoke Pos. Distortion Mult. + #LOC_BDArmory_Settings_APSThreshold = Min. Caliber to Trigger APS } } diff --git a/BDArmory/Distribution/GameData/BDArmory/Localization/UI/ru-ru.cfg b/BDArmory/Distribution/GameData/BDArmory/Localization/UI/ru-ru.cfg new file mode 100644 index 000000000..0c0737eb8 --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Localization/UI/ru-ru.cfg @@ -0,0 +1,1039 @@ +Localization +{ + ru + { + #LOC_BDArmory_Generic_Cancel = Отменить + #LOC_BDArmory_Generic_New = Новый + #LOC_BDArmory_Generic_On = Вкл + #LOC_BDArmory_Generic_Off = Выкл + #LOC_BDArmory_Generic_Hide = Спрятать + #LOC_BDArmory_Generic_Show = Показать + #LOC_BDArmory_VesselStatus_Landed = (Посажен) + #LOC_BDArmory_VesselStatus_Splashed = (Уничтожен) + #LOC_BDArmory_VesselStatus_Underwater = (Под водой) + + #LOC_BDArmory_WMWindow_title = BDA Контроллер Вооружения + #LOC_BDArmory_WMWindow_GuardModebtn = Боевой Режим + #LOC_BDArmory_WMWindow_ArmedText = Триггер\u0020 + #LOC_BDArmory_WMWindow_ArmedText_ARMED = ВООРУЖЕН. + #LOC_BDArmory_WMWindow_ArmedText_DisArmed = разоружен. + #LOC_BDArmory_WMWindow_TeamText = Команда + #LOC_BDArmory_WMWindow_selectionText = Оружие: <<1>> + #LOC_BDArmory_WMWindow_rippleText1 = Поочередный огонь: <<1>> RPM + #LOC_BDArmory_WMWindow_rippleText2 = Залп + #LOC_BDArmory_WMWindow_rippleText3 = Залповый огонь: <<1>> RPM + #LOC_BDArmory_WMWindow_rippleText4 = Залповый огонь: ВЫКЛ + #LOC_BDArmory_WMWindow_barrageStagger = Колебания + #LOC_BDArmory_WMWindow_ListWeapons = Вооружение + #LOC_BDArmory_WMWindow_GuardMenu = Боевое Меню + #LOC_BDArmory_WMWindow_ModulesToggle = Модули + #LOC_BDArmory_WMWindow_NoneWeapon = Нет + #LOC_BDArmory_WMWindow_NoneWeapon = Боевой Режим <<1>> + #LOC_BDArmory_WMWindow_FiringInterval = Огневой Интервал + #LOC_BDArmory_WMWindow_BurstLength = Длина Очереди + #LOC_BDArmory_WMWindow_FiringTolerance = Огневой Угол + #LOC_BDArmory_FiringPriority = Приоритет Использования + #LOC_BDArmory_WMWindow_FieldofView = Поле Зрения + #LOC_BDArmory_WMWindow_VisualRange = Визуальная Дистанция + #LOC_BDArmory_WMWindow_GunsRange = Дальность Действия Пулеметов + #LOC_BDArmory_WMWindow_MultiTargetNum = Макс. кол-во целей Турели + #LOC_BDArmory_WMWindow_MultiMissileNum = Макс. кол-во Ракетных целей + + #LOC_BDArmory_WMWindow_MissilesTgt = Ракеты/Цель + #LOC_BDArmory_WMWindow_TargetType = Тип Цели: + #LOC_BDArmory_WMWindow_TargetType_Missiles = Ракеты + #LOC_BDArmory_WMWindow_TargetType_All = Все Цели + #LOC_BDArmory_WMWindow_RadarWarning = Приемник Радиолокационного Предупреждения + #LOC_BDArmory_WMWindow_GPSCoordinator = GPS Координатор + #LOC_BDArmory_WMWindow_WingCommand = Управление Крылом + #LOC_BDArmory_WMWindow_NoWeaponManager = Контроллер Вооружения не найден. + #LOC_BDArmory_WMWindow_GPSTarget = GPS Цель + #LOC_BDArmory_WMWindow_NoTarget = Нет Цели + + //WM inflolin + #LOC_BDArmory_WMWindow_Weapons_Desc = Вооружение - Эта вкладка отображает все оружия/группы вооружения на судне. При нажатии на название оружия (группы) оно будет выбрано и активировано, позволяя вручную стрелять из пулемета, ракетной установки или лазера. Если выбрана ракета, переключатель "Триггер разоружен" должен быть переключен на ВООРУЖЕН, прежде чем ракета может быть запущена. + + #LOC_BDArmory_WMWindow_Ripple_Salvo_Desc = Ракеты, когда выбраны, будут иметь опцию Залпового огня. Она отвечает за залповый пуск ракет, если триггер вооружен. Пулеметы, ракетные установки и лазеры со скорострельностью ниже 1500 выстрелов в минуту будут иметь переключатель Последовательный огонь/Залп. Если присутствует несколько видов оружия одного типа/группы оружия, режим Последовательного огня приведет к тому, что каждое оружие будет стрелять последовательно. В режиме Залпа все орудия будут стрелять одновременно. + + #LOC_BDArmory_WMWindow_GuardTab_Desc = Боевой Режим - эта группа настроек контролирует, как и когда ИИ будет использовать вооружение на Вашем аппарате. + #LOC_BDArmory_WMWindow_FiringInterval_Desc = Огневой Интервал - Контролирует, как часто, в секундах, ИИ будет искать цель. Другими словами, как часто он будет стрелять из выбранного оружия. + #LOC_BDArmory_WMWindow_BurstLength_desc = Длина Очереди - Контролирует, как долго, в секундах, ИИ будет продолжать стрелять из выбранного оружия. Если установлено значение 0, ИИ будет стрелять (1/2 * Огневой Интервал) секунд. + #LOC_BDArmory_WMWindow_FiringTolerance_desc = Огневой Угол - Контролирует, когда ИИ считает, что навелся на цель, и начинает стрелять. Угол 1 означает, что цель должна находиться внутри прицела, который эквивалентен по ширине сумме радиуса цели и разброса выбранного оружия. Более точные оружия имеют более узкий прицел, и наоборот. Угол 2 означает прицел, в два раза больший ширины радиуса цели, и так далее. Когда цель окажется внутри прицела, ИИ будет стрелять из выбранного оружия. + #LOC_BDArmory_WMWindow_FieldofView_desc = Поле Зрения - Контролирует поле зрения ИИ. Значение 360 означает, что он видит все во всех направлениях; значение ниже, чем 360, означает, что ИИ может видеть цели только под таким углом. + #LOC_BDArmory_WMWindow_VisualRange_desc = Визуальная Дистанция - Контролирует, как далеко ИИ может видеть. Цели, находящиеся ближе этого расстояния, будут видны, и ИИ будет атаковать цель. Цели, находящиеся дальше визуальной дистанции, нужно будет обнаруживать радаром. + #LOC_BDArmory_WMWindow_GunsRange_desc = Дальность Действия Пулеметов - Устанавливает максимальную дальность для всех пулеметов, ракетных установок и лазеров на судне. По умолчанию будет установлена наибольшая дальность действия оружия (кроме ракет), установленного на аппарате. ИИ не будет пытаться атаковать цели за пределом этой дистанции. + #LOC_BDArmory_WMWindow_MultiTargetNum_desc = Макс. кол-во целей Турели - На аппаратах с несколькими турелями устанавливает, сколько целей турели могут независимо атаковать, позволяя аппарату атаковать несколько целей одновременно. + #LOC_BDArmory_WMWindow_MultiMissileTgtNum_desc = Макс. кол-во Ракетных целей - На аппаратах с несколькими ракетами устанавливает, сколько разных целей ИИ будет искать, чтобы атаковать ракетами, переключаясь на новую цель, как только установленное количество ракет выпущено по текущей цели. + #LOC_BDArmory_WMWindow_MissilesTgt_desc = Ракеты/Цель - Устанавливает, сколько ракет ИИ будет выпускать по одной цели. Когда нужное количество ракет запущено, ИИ будет запускать новые ракеты только после того, как прежде запущенная ракета поразит цель или будет уничтожена. + #LOC_BDArmory_WMWindow_TargetType_desc = Кнопка "Расширенное Наведение" - Позволяет настраивать приоритеты в наведении для ИИ, позволяя ему атаковать конкретно оружие, двигатели, командные модули, массивные детали или какие-то комбинации из вышеперечисленного, вместо того, чтобы атаковать Центр Масс. + #LOC_BDArmory_WMWindow_EngageType_desc = Кнопка "Настройки Атакуемых Целей" - Переключатель, чтобы быстро устанавливать атакуемые цели (Воздушные, Наземные, Ракеты или Торпеды) для всего вооружения сразу. + + #LOC_BDArmory_WMWindow_TargetPriority = Приоритетные Цели + #LOC_BDArmory_WMWindow_targetBias = Смена Цели + #LOC_BDArmory_WMWindow_targetPreference = Атаковать Воздушные Цели + #LOC_BDArmory_WMWindow_targetProximity = Дистанция до Цели + #LOC_BDArmory_WMWindow_targetAngletoTarget = Угол до Цели + #LOC_BDArmory_WMWindow_targetAngleDist = Угол / Дистанция + #LOC_BDArmory_WMWindow_targetAccel = Target TWR + #LOC_BDArmory_WMWindow_targetClosingTime = Время Сближения + #LOC_BDArmory_WMWindow_targetgunNumber = Количество Вооружения + #LOC_BDArmory_WMWindow_targetMass = Масса Цели + #LOC_BDArmory_WMWindow_targetAllies = Наименьшее кол-во Союзников Атакует + #LOC_BDArmory_WMWindow_targetThreat = Угроза Цели + #LOC_BDArmory_WMWindow_defendTeammate = Защищать Союзника + #LOC_BDArmory_WMWindow_targetVIP = Атаковать VIP + #LOC_BDArmory_WMWindow_defendVIP = Защищать VIP + + #LOC_BDArmory_WMWindow_Prioritues_Desc = Приоритетность Целей - Эта вкладка контролирует приоритетные цели для ИИ. + #LOC_BDArmory_WMWindow_targetBias_desc = Смена Цели - Устанавливает, как сильно ИИ будет предпочитать текущую цель потенциальной новой цели. Чем выше значение, тем больше ИИ предпочитает текущую цель. + #LOC_BDArmory_WMWindow_targetPreference_desc = Приоритетный Тип Цели - Устанавливает приоритетный тип цели для ИИ. Чем ниже значение, тем больше ИИ будет предпочитать атаковать Наземные цели, чем выше значение, тем больше приоритет у Воздушных целей. + #LOC_BDArmory_WMWindow_targetProximity_desc = Дистанция до Цели - Устанавливает приоритетную дистанцию до цели для ИИ. Установите большее значение, чтобы атаковать ближайшие цели, меньшее значение, чтобы предпочитать более далекие. + #LOC_BDArmory_WMWindow_targetAngletoTarget_desc = Угол до Цели - Контролирует приоритетность целей, находящихся под наименьшим углом от направления аппарата. Чем выше значение, больше приоритетность у целей, находящихся прямо перед аппаратом. + #LOC_BDArmory_WMWindow_targetAngleDist_desc = Угол / Дистанция - Контролирует приоритетность целей, основываясь на значении угла до цели, разделенном на расстояние до цели. Большие значения отдают приоритет ближайшим целям под небольшим углом, маленькие - наоборот. + #LOC_BDArmory_WMWindow_targetAccel_desc = Ускорение Цели - Большие значения отдают приоритет целям с наибольшим ускорением, меньшие значения - более медленным целям. + #LOC_BDArmory_WMWindow_targetClosingTime_desc = Наименьшее Время Сближения - Большие значения отдают приоритет целям, которые можно достичь быстрее всех, меньшие значения - целям, до которых лететь дольше. + #LOC_BDArmory_WMWindow_targetgunNumber_desc = Количество Вооружения - Контролирует приоритетность целей, основываясь на количестве оружия у цели. Большие значения отдают приоритет целям с наибольшим количеством оружия, меньшие значения - целям с наименьшим количеством оружия. + #LOC_BDArmory_WMWindow_targetMass_desc = Масса Цели - Контролирует приоритетность целей, основываясь на массе цели. Большие значения отдают приоритет более тяжелым целям. + #LOC_BDArmory_WMWindow_targetAllies_desc = Наименьшее кол-во Союзников Атакует - Контролирует приоритетность целей, основываясь на количестве союзников, атакующих эту цель. Большие значения отдают приоритет никем не атакуемым целям, меньшие значения - целям, атакованным союзниками. + #LOC_BDArmory_WMWindow_targetThreat_desc = Угроза Цели - Большие значения отдают приоритет целям, атакующим данный аппарат, меньшие значения игнорируют атакующие данный аппарат цели. + #LOC_BDArmory_WMWindow_targetVIP_desc = Атаковать VIP / Защищать VIP. Отдает приоритет атаке вражеских VIP целей или атаке целей, которые атакуют VIP союзника, если установлены большие значения, и игнорирует их, если установлены маленькие значения. + + #LOC_BDArmory_Settings_Title = Настройки BDArmory + #LOC_BDArmory_Settings_GeneralSettingsToggle = Настройки Игрового Процесса + #LOC_BDArmory_Settings_GraphicsSettingsToggle = Настройки Графики/Интерфейса + #LOC_BDArmory_Settings_SliderSettingsToggle = Основные Значения + #LOC_BDArmory_Settings_RadarSettingsToggle = Настройки Радара + #LOC_BDArmory_Settings_GameModesSettingsToggle = Игровые Режимы + #LOC_BDArmory_Settings_OtherSettingsToggle = Другие Настройки + #LOC_BDArmory_Settings_CompSettingsToggle = Настройки Состязаний + #LOC_BDArmory_Settings_GMSettingsToggle = Настройки Ограничений + #LOC_BDArmory_Settings_DebugSettingsToggle = Отладка + #LOC_BDArmory_Settings_AIToolbarButton = Кнопка ИИ на Панели Инструментов + #LOC_BDArmory_Settings_Instakill = Мгновенное Убийство + #LOC_BDArmory_Settings_InfiniteAmmo = Бесконечные Боеприпасы + #LOC_BDArmory_Settings_BulletFX = Bullet FX + #LOC_BDArmory_Settings_BulletHits = Пулевые Попадания + #LOC_BDArmory_Settings_EjectShells = Отбрасывать Гильзы + #LOC_BDArmory_Settings_VesselRelativeBulletChecks = Проверять Пули на Аппарате + #LOC_BDArmory_Settings_AimAssist = Помощь в Наведении + #LOC_BDArmory_Settings_GUIBackgroundOpacity = Непрозрачность Фона Интерфейса + #LOC_BDArmory_Settings_DrawAimers = Целеуказатели + #LOC_BDArmory_Settings_DebugTelemetry = Экранная Телеметрия + #LOC_BDArmory_Settings_DebugLines = Отладка Линий + #LOC_BDArmory_Settings_DebugAI = ИИ + #LOC_BDArmory_Settings_DebugArmor = Броня + #LOC_BDArmory_Settings_DebugCompetition = Состязание + #LOC_BDArmory_Settings_DebugDamage = Урон + #LOC_BDArmory_Settings_DebugMissiles = Ракеты + #LOC_BDArmory_Settings_DebugOther = Другое + #LOC_BDArmory_Settings_DebugRadar = Детекторы + #LOC_BDArmory_Settings_DebugSpawning = Спавн + #LOC_BDArmory_Settings_DebugWeapons = Оружие + #LOC_BDArmory_Settings_RemoteFiring = Дистанционная Стрельба + #LOC_BDArmory_Settings_ClearanceCheck = Проверка Клиренса + #LOC_BDArmory_Settings_AmmoGauges = Датчики Боеприпасов + #LOC_BDArmory_Settings_GaplessParticleEmitters = Бесщелевые Излучатели Частиц + #LOC_BDArmory_Settings_FlareSmoke = Дым от Тепловых Ловушек + #LOC_BDArmory_Settings_ShellCollisions = Столкновение Снарядов + #LOC_BDArmory_Settings_BulletHoleDecals = Отверстия от Пуль + #LOC_BDArmory_Settings_PerformanceLogging = Журнал Производительности + #LOC_BDArmory_Settings_StrictWindowBoundaries = Строгие Границы Окна + #LOC_BDArmory_Settings_PersistentFX = Стабильные Эффекты + #LOC_BDArmory_Settings_DisableKillTimer = Отключить Таймер Убийства + #LOC_BDArmory_Settings_TraceVessels = Автоматическая активация Отслеживания Аппаратов + #LOC_BDArmory_Settings_TraceVesselsManualStart = Начать Отслеживание + #LOC_BDArmory_Settings_TraceVesselsManualStop = Остановить Отслеживание + #LOC_BDArmory_Settings_ShowEditorSubcategories = Показать Подкатегории для Разработчика + #LOC_BDArmory_Settings_AutocategorizeParts = Автоматическая Классификация Деталей + #LOC_BDArmory_Settings_waterDrag = Подводное Сопротивление Пуль + #LOC_BDArmory_Settings_AutoLoadToKSC = Автоматическая Загрузка в Космический Центр + #LOC_BDArmory_Settings_GenerateCleanSave = Создать Чистое Сохранение + #LOC_BDArmory_Settings_AutoResumeTournaments = Автоматическое Продолжение Турнира + #LOC_BDArmory_Settings_AutoQuitAtEndOfTournament = Автоматический Выход в Конце Турнира + #LOC_BDArmory_Settings_AutoQuitMemoryUsage = Порог Автоматического Выхода из Памяти + #LOC_BDArmory_Settings_CurrentMemoryUsageEstimate = Оценка Текущего Использования Памяти + #LOC_BDArmory_Settings_TimeOverride = Изменение Времени + #LOC_BDArmory_Settings_TimeScale = Ускорение Времени + #LOC_BDArmory_Settings_legacyArmor = Вкл Устаревшую Броню + #LOC_BDArmory_Settings_AdvancedUserSettings = Расширенные Настройки + #LOC_BDArmory_Settings_DamageMultiplier = Множитель Урона + #LOC_BDArmory_Settings_ExtraDamageSliders = Дополнительные Настройки Урона + #LOC_BDArmory_Settings_BallisticDamageMultiplier = Множитель Баллистического Урона + #LOC_BDArmory_Settings_ExplosiveDamageMultiplier = Множитель Взрывного Урона + #LOC_BDArmory_Settings_RocketExplosiveDamageMultiplier = Множитель Взрывного Урона Ракет из Пусковой Установки + #LOC_BDArmory_Settings_MissileExplosiveDamageMultiplier = Множитель Взрывного Урона Ракет + #LOC_BDArmory_Settings_ExplosiveBattleDamageMultiplier = Б.П. Множитель Взрывного Урона + #LOC_BDArmory_Settings_ImplosiveDamageMultiplier = Множитель Имплозивного Урона + #LOC_BDArmory_Settings_SecondaryEffectDuration = Продолжительность Эффектов Особого Вооружения + #LOC_BDArmory_Settings_BallisticTrajectorSimulationMultiplier = Множитель Баллистической Траектории + #LOC_BDArmory_Settings_DebrisCleanUpDelay = Задержка Удаления Обломков + #LOC_BDArmory_Settings_Scoring_HeadShot = Ограничение Времени Попадания в Голову + #LOC_BDArmory_Settings_Scoring_KillSteal = Ограничение Времени Кражи Убийства + #LOC_BDArmory_Settings_MaxBulletHoles = Макс. кол-во Пулевых Отверстий + #LOC_BDArmory_Settings_TerrainAlertFrequency = Частота Проверки Ландшафта + #LOC_BDArmory_Settings_CameraSwitchFrequency = Частота Переключения Камеры + #LOC_BDArmory_Settings_DeathCameraInhibitPeriod = Период Наблюдения за Уничтоженным Аппаратом + #LOC_BDArmory_Settings_Max_PWing_HP = Макс. Здоровье Процедурных Деталей + #LOC_BDArmory_Settings_HP_Clamp = Макс. Предел Здоровья + #LOC_BDArmory_Settings_DisableRamming = Отключить Таран + #LOC_BDArmory_Settings_DefaultFFATargeting = Нацеливание По Умолчанию + #LOC_BDArmory_Settings_TagMode = Режим Пятнашек + #LOC_BDArmory_Settings_PaintballMode = Режим Пейнтбола + #LOC_BDArmory_Settings_DumbIRSeekers = Отк. Отклонение Тепловых Ловушек + #LOC_BDArmory_Settings_RunwayProject = Runway Project + #LOC_BDArmory_Settings_RunwayProjectRound = Раунд Runway Project + #LOC_BDArmory_Settings_BattleDamage = Боевые Повреждения + #LOC_BDArmory_Settings_GravityHacks = Усилить Гравитацию при Смерти + #LOC_BDArmory_Settings_AutoEnableVesselSwitching = Автоматическое Переключение Аппаратов + #LOC_BDArmory_Settings_AutonomousCombatSeats = Автономные Боевые Кресла + #LOC_BDArmory_Settings_DestroyWMWhenNotControlled = Уничтожить Неконтролируемые Аппараты + #LOC_BDArmory_Settings_DisplayCompetitionStatus = Отображать Статус Состязания + #LOC_BDArmory_Settings_DisplayCompetitionStatusHiddenUI = Отображать Статус при Выключенном Интерфейсе + #LOC_BDArmory_Settings_ResetHP = Установить Макс. Здоровье Всем Деталям + #LOC_BDArmory_Settings_ResetArmor = Сбросить Значения Брони + #LOC_BDArmory_Settings_ResetHull = Сбросить Материалы + #LOC_BDArmory_Settings_IntakeHack = Хакерские Заборники + #LOC_BDArmory_Settings_KerbalSafety = Безопасность Кербонавтов + #LOC_BDArmory_Settings_KerbalSafetyInventory = Инвентарь Кербонавтов + #LOC_BDArmory_Settings_KerbalSafetyInventory_NoChange = Нет Заряда + #LOC_BDArmory_Settings_KerbalSafetyInventory_ResetDefault = Сбросить Значения + #LOC_BDArmory_Settings_KerbalSafetyInventory_ChuteOnly = Только Парашют + #LOC_BDArmory_Settings_PeaceMode = Мирный Режим + #LOC_BDArmory_settings_FireRate = Управление Скорострельностью + #LOC_BDArmory_settings_FireRateCenter = Управление Средней Скорострельностью + #LOC_BDArmory_settings_FireRateSpread = Управление Разбросом Скорострельности + #LOC_BDArmory_settings_FireRateBias = Управление Отклонением Скорости Стрельбы + #LOC_BDArmory_settings_FireRateHitMultiplier = Множитель Скорострельности + #LOC_BDArmory_settings_ZombieMode = Режим Зомби + #LOC_BDArmory_settings_zombieDmgMod = Множитель Урона от Зомби + + #LOC_BDArmory_Settings_BDSettingsToggle = Настройки Боевых Повреждений + #LOC_BDArmory_Settings_BD_Proc = Частота Процесса + #LOC_BDArmory_Settings_BD_Engines = Повреждения Двигателей + #LOC_BDArmory_Settings_BD_Prop_Dmg_Mult = Количество Повреждений Двигателя + #LOC_BDArmory_Settings_BD_Prop_floor = Минимальная Тяга Двигателя + #LOC_BDArmory_Settings_BD_Prop_flameout = Срыв Пламени + #LOC_BDArmory_Settings_BD_Intakes = Повреждения Заборников + #LOC_BDArmory_Settings_BD_Gimbals = Повреждения Подвески + #LOC_BDArmory_Settings_BD_Aero = Повреждения Летных Систем + #LOC_BDArmory_Settings_BD_Aero_Dmg_Mult = Количество Повреждений Крыльев + #LOC_BDArmory_Settings_BD_CtrlSrf = Повреждения Элевонов + #LOC_BDArmory_Settings_BD_Command = Повреждения Командных Деталей + #LOC_BDArmory_Settings_BD_PilotKill = Гибель Экипажа + #LOC_BDArmory_Settings_BD_Tanks = Повреждения Топливных Баков + #LOC_BDArmory_Settings_BD_Leak_Rate = Количество Утечек + #LOC_BDArmory_Settings_BD_Leak_Time = Продолжительность Утечек + #LOC_BDArmory_Settings_BD_SubSystems = Повреждения Вспомогательных Систем + #LOC_BDArmory_Settings_BD_Ammo = Взрыв Боеприпасов + #LOC_BDArmory_Settings_BD_Volatile_Ammo = Ящики с Боеприпасами Взрываются при Уничтожении + #LOC_BDArmory_Settings_BD_Ammo_Mult = Урон от Взрывов + #LOC_BDArmory_Settings_BD_Fires = Огонь + #LOC_BDArmory_Settings_BD_DoT = Урон от Огня + #LOC_BDArmory_Settings_BD_Fire_Dmg = Урон от Огня/сек + #LOC_BDArmory_Settings_BD_FireHeat = Огонь Увеличивает Температуру + #LOC_BDArmory_Settings_BD_FuelFireEX = Взрыв Топлива + #LOC_BDArmory_Settings_BD_ZombieMode = Разрешить Боевые Повреждения + + #LOC_BDArmory_Settings_Adv_Targeting = Продвинутое Наведение + #LOC_BDArmory_Selecttargeting = Выбрать Настройку Наведения + #LOC_BDArmory_targetSetting = Наведение + #LOC_BDArmory_TargetCOM = Центр Масс + #LOC_BDArmory_Weapons = Оружие + #LOC_BDArmory_Engines = Двигатели + #LOC_BDArmory_Command = Кабины + #LOC_BDArmory_Mass = Массивные Детали + #LOC_BDArmory_Random = Случайные Детали + + #LOC_BDArmory_Ammo_Setup = Конфигурация Боеукладки + #LOC_BDArmory_Ammo_Weapon = Выбранное Оружие: + #LOC_BDArmory_Ammo_Belt = Текущий Пояс: + #LOC_BDArmory_advanced = Конфигурация Боеприпасов: Расширенная + #LOC_BDArmory_simple = Конфигурация Боеприпасов: Обычная + #LOC_BDArmory_useBelt = Использует Пользовательскую Боеукладку: + #LOC_BDArmory_save = Сохранить + #LOC_BDArmory_reset = Сбросить + + #LOC_BDArmory_BDAVesselSpawner_Title = BDA Спавнер Аппаратов + #LOC_BDArmory_Settings_SpawnOptions = Параметры Размещения + #LOC_BDArmory_Settings_VesselSpawnGeoCoords = Установить Точку Размещения Здесь + #LOC_BDArmory_Settings_SpawnDistanceFactor = Коэфицент Дальности Размещения + #LOC_BDArmory_Settings_SpawnDistance = Дальность Размещения + #LOC_BDArmory_Settings_SpawnDistanceToggle = Абсолютное Расстояние vs Фактор + #LOC_BDArmory_Settings_SpawnReassignTeams = Переназначить Команды + #LOC_BDArmory_Settings_SpawnEaseInSpeed = Скорость Спавнера + #LOC_BDArmory_Settings_SpawnConcurrentVessels = Конкурентный Аппарат (CS) + #LOC_BDArmory_Settings_SpawnLivesPerVessel = Жизни Аппарата (CS) + #LOC_BDArmory_Settings_SpawnDumpLogsEverySpawn = Регистрировать Каждое Размещение (CS) + #LOC_BDArmory_Settings_SpawnContinueSingleSpawning = Непрерывное Размещение (S) + #LOC_BDArmory_Settings_SpawnRandomOrder = Случайный Порядок Размещения (S) + #LOC_BDArmory_Settings_SpawnSpawnProbeHere = Разместить Спавнер Здесь + #LOC_BDArmory_Settings_OutOfAmmoKillTime = Время Убийства из-за Отсутствия Боеприпасов (CS) + #LOC_BDArmory_Settings_ClearDebrisNow = Удалить Обломки + #LOC_BDArmory_Settings_ClearBystandersNow = Удалить Незадействованные Аппараты + #LOC_BDArmory_Settings_SaveSpawnLoc = Сохранить Локацию + #LOC_BDArmory_Settings_WarpHere = Переместить Сюда + #LOC_BDArmory_Settings_Planet = Выбрать Планету + + #LOC_BDArmory_Settings_SpawnFillSeats = Заполнить Места + #LOC_BDArmory_Settings_SpawnFillSeats_Minimal = По Минимуму + #LOC_BDArmory_Settings_SpawnFillSeats_Default = Кабины Пилотов и Боевые Кресла + #LOC_BDArmory_Settings_SpawnFillSeats_AllControlPoints = Все Точки Управления + #LOC_BDArmory_Settings_SpawnFillSeats_Cabins = Также Кабины + #LOC_BDArmory_Settings_Teams = Команды + #LOC_BDArmory_Settings_Teams_FFA = FFA + #LOC_BDArmory_Settings_Teams_Folders = Для Каждой Папки / Для Каждого Файла + #LOC_BDArmory_Settings_Teams_SplitEvenly = Разделить Равномерно на + #LOC_BDArmory_Settings_SpawnFilesLocation = Расположение Файлов Аппаратов + + #LOC_BDArmory_Settings_SpawnLocations = Интересные Места Размещения + #LOC_BDArmory_Settings_SingleSpawn = Одиночное Размещение + #LOC_BDArmory_Settings_ContinuousSpawning = Продолжительное Размещение + #LOC_BDArmory_Settings_CancelSpawning = Отменить Размещение + + // Waypoints game mode + #LOC_BDArmory_Settings_WaypointsMode = Режим Вейпоинтов + #LOC_BDArmory_Settings_WaypointsOptions = Настройки Вейпоинтов + #LOC_BDArmory_Settings_WaypointsOneAtATime = По Одному за Раз + #LOC_BDArmory_Settings_WaypointsInfFuelAtStart = Бесконечное Топливо до Первого Вейпоинта + #LOC_BDArmory_Settings_WaypointsShow = Показать Вейпоинты + + // Tournaments + #LOC_BDArmory_Settings_TournamentOptions = Настройки Турнира + #LOC_BDArmory_Settings_TournamentStyle = Стиль Турнира + #LOC_BDArmory_Settings_TournamentDelayBetweenHeats = Задержка Между Нагревами + #LOC_BDArmory_Settings_TournamentTimeWarpBetweenRounds = Ускорение Времени между Раундами + #LOC_BDArmory_Settings_TournamentRounds = Раунды + #LOC_BDArmory_Settings_TournamentVesselsPerHeat = Аппараты на Нагрев + #LOC_BDArmory_Settings_TournamentVesselsPerTeam = Аппараты на Команду на Нагрев + #LOC_BDArmory_Settings_TournamentTeamsPerHeat = Команды на Нагрев + #LOC_BDArmory_Settings_GauntletOpponentsFilesLocation = Вызывать Файлы Оппонента + #LOC_BDArmory_Settings_TournamentOpponentTeamsPerHeat = Команды Оппонента на Нагрев + #LOC_BDArmory_Settings_TournamentOpponentVesselsPerTeam = Аппапары Оппонента на Команду + #LOC_BDArmory_Settings_TournamentFullTeams = Использовать Аппараты Снова для Заполнения Команд + #LOC_BDArmory_Settings_TournamentSetup = Устроить Турнир + #LOC_BDArmory_Settings_TournamentRun = Начать Турнир + #LOC_BDArmory_Settings_TournamentStop = Остановить Турнир + + #LOC_BDArmory_Settings_HeartBleed = Кровотечение + #LOC_BDArmory_Settings_HeartBleedRate = Частота Кровотечения + #LOC_BDArmory_Settings_HeartBleedInterval = Задержка Кровотеченя + #LOC_BDArmory_Settings_HeartBleedThreshold = Порог Кровотечения + + #LOC_BDArmory_Settings_ResourceSteal = Кража Ресурсов + #LOC_BDArmory_Settings_ResourceSteal_RespectFlowStateIn = Почитать Входящий Поток + #LOC_BDArmory_Settings_ResourceSteal_RespectFlowStateOut = Почитать Выходящий Поток + #LOC_BDArmory_Settings_FuelStealRation = Норма Кражи Топлива + #LOC_BDArmory_Settings_AmmoStealRation = Норма Кражи Боеприпасов + #LOC_BDArmory_Settings_CMStealRation = Норма Кражи Контрмер + + #LOC_BDArmory_Settings_AsteroidField = Астероидное Поле + #LOC_BDArmory_Settings_AsteroidFieldNumber = Количество Астероидов + #LOC_BDArmory_Settings_AsteroidFieldAltitude = Высота Астероидного Поля + #LOC_BDArmory_Settings_AsteroidFieldRadius = Радиус Астероидного Поля + #LOC_BDArmory_Settings_AsteroidFieldAnomalousAttraction = Аномальное Притяжение + #LOC_BDArmory_Settings_AsteroidRain = Метеоритный Дождь + #LOC_BDArmory_Settings_AsteroidRainNumber = Количество Метеоритов + #LOC_BDArmory_Settings_AsteroidRainAltitude = Высота Метеоритного Дождя + #LOC_BDArmory_Settings_AsteroidRainRadius = Радиус Метеоритного Дождя + #LOC_BDArmory_Settings_AsteroidRainFollowsCentroid = Отслеживает Местоположение Аппаратов + #LOC_BDArmory_Settings_AsteroidRainFollowsSpread = Отслеживает Разброс Аппаратов + + #LOC_BDArmory_Settings_RWRWindowScale = Размер Окна ПРП + #LOC_BDArmory_Settings_RadarWindowScale = Размер Окна Радара + #LOC_BDArmory_Settings_LogarithmicRWRDisplay = Логарифмический Дисплей ПРП + #LOC_BDArmory_Settings_TargetWindowScale = Размер Окна Цели + #LOC_BDArmory_Settings_TargetWindowInvertMouse = Инвертировать Управление (Окно Наведения) + #LOC_BDArmory_Settings_TriggerHold = Удержание Триггера + #LOC_BDArmory_Settings_UIVolume = Громкость Интерфейса + #LOC_BDArmory_Settings_WeaponVolume = Громкость Оружия + + #LOC_BDArmory_Settings_CompetitionDistance = Дистанция Состязания + #LOC_BDArmory_Settings_CompetitionDuration = Продолжительность Состязания + #LOC_BDArmory_Settings_CompetitionFinalGracePeriod = Финальный Мирный Период + #LOC_BDArmory_Settings_CompetitionInitialGracePeriod = Начальный Мирный Период + #LOC_BDArmory_Settings_CompetitionKillTimer = Таймер Убийства Посаженного Аппарата + #LOC_BDArmory_Settings_CompetitionNonCompetitorRemovalDelay = Задержка Удаления Незадействованных Аппаратов + #LOC_BDArmory_Settings_CompetitionKillerGMFrequency = Killer GM Frequency + #LOC_BDArmory_Settings_CompetitionKillerGMGracePeriod = Killer GM Grace Period + #LOC_BDArmory_Settings_CompetitionAltitudeLimitHigh = Максимальная Высота + #LOC_BDArmory_Settings_CompetitionAltitudeLimitLow = Минимальная Высота + #LOC_BDArmory_Settings_CompetitionStarting = Начало Состязания... + #LOC_BDArmory_Settings_SpawnStartCompetitionAutomatically = Начинать Состязания Автоматически + #LOC_BDArmory_Settings_DogfightCompetition = Воздушный Бой + #LOC_BDArmory_Settings_StartCompetition = Начать Состязание + #LOC_BDArmory_Settings_StopCompetition = Остановить Состязание + #LOC_BDArmory_Settings_StartCompetitionNow = Начать Состязание СЕЙЧАС + #LOC_BDArmory_Settings_CompetitionStartNowAfter = Задержка Ручного Начала Состязания + #LOC_BDArmory_Settings_StartRapidDeployment = Начать Оперативное Развертывание + #LOC_BDArmory_Settings_StartOrbitalDeployment = Начать Орбитальное Развертывание + #LOC_BDArmory_Settings_LowGravDeployment = Начать Состязание с Низкой Гравитацией + #LOC_BDArmory_Settings_EditInputs = Редактировать Входные Данные + #LOC_BDArmory_Settings_CompetitionCloseSettingsOnCompetitionStart = Закрывать Окно Настроек при Начале Состязания + + #LOC_BDArmory_BDARemoteOrchestration_Title = BDA Удаленная Аранжировка + #LOC_BDArmory_Settings_RemoteLogging = Удаленная Аранжировка + #LOC_BDArmory_Settings_RemoteInterheatDelay = Задержка между Нагревами + #LOC_BDArmory_Settings_RemoteSync = Запустить через Удаленную Аранжировку + #LOC_BDArmory_Settings_CompetitionID = ID Состязания + + //Evolution + #LOC_BDArmory_Evolution_Title = Развитие BDA + #LOC_BDArmory_Evolution_Options = Настройки Развития + #LOC_BDArmory_Evolution_HeatsPerGroup = Нагрев на Группу + #LOC_BDArmory_Evolution_MutationsPerHeat = Мутации за Нагрев + #LOC_BDArmory_Evolution_AdversariesPerHeat = Противники за Нагрев + #LOC_BDArmory_Evolution_ID = Развитие + #LOC_BDArmory_Evolution_Status = Статус + #LOC_BDArmory_Evolution_Group = Группа + #LOC_BDArmory_Evolution_Heat = Нагрев + + #LOC_BDArmory_Generic_SaveandClose = Сохранить и Выйти + + #LOC_BDArmory_InputSettings_Weapons = Вооружение + #LOC_BDArmory_InputSettings_TargetingPod = Модуль Наведения + #LOC_BDArmory_InputSettings_Radar = Радар + #LOC_BDArmory_InputSettings_VesselSwitcher = Переключатель Аппаратов + #LOC_BDArmory_InputSettings_Tournament = Турнир + #LOC_BDArmory_InputSettings_TimeScaling = Изменение Времени + #LOC_BDArmory_InputSettings_GUI = Интерфейс + #LOC_BDArmory_InputSettings_BackBtn = Назад + #LOC_BDArmory_InputSettings_recordedInput = Нажмите кнопку. + #LOC_BDArmory_InputSettings_SetKey = Установить Кнопку + #LOC_BDArmory_InputSettings_Clear = Очистить + + #LOC_BDArmory_ProtoStageIconInfo_Reloading = Перезарядка + #LOC_BDArmory_ProtoStageIconInfo_Overheat = Перегрев + #LOC_BDArmory_ProtoStageIconInfo_AmmoOut = Нет Боеприпасов + + #LOC_BDArmory_WingCommander_Title = Командование Крылом + #LOC_BDArmory_WingCommander_Guiname1 = Разброс в Строю + #LOC_BDArmory_WingCommander_Guiname2 = Отставание в Строю + #LOC_BDArmory_WingCommander_Guiname3 = Переключить Интерфейс + #LOC_BDArmory_WingCommander_SelectAll = Выбрать Все + #LOC_BDArmory_WingCommander_CommandSelf = Командовать Самостоятельно + #LOC_BDArmory_WingCommander_Follow = Следовать + #LOC_BDArmory_WingCommander_FlyToPos = Лететь на Позицию + #LOC_BDArmory_WingCommander_AttackPos = Атаковать Позицию + #LOC_BDArmory_WingCommander_ActionGroup = Группа Действий + #LOC_BDArmory_WingCommander_ActionGroups = Группы Действий + #LOC_BDArmory_WingCommander_TakeOff = Взлететь + #LOC_BDArmory_WingCommander_Release = Отпустить + #LOC_BDArmory_WingCommander_FormationSettings = Настройки Полета в Строю + #LOC_BDArmory_WingCommander_Spread = Разброс + #LOC_BDArmory_WingCommander_Lag = Отставание + #LOC_BDArmory_WingCommander_ScreenMessage = Выберите координаты цели.\nПКМ для отмены. + + #LOC_BDArmory_BDAVesselSwitcher_Title = BDA Переключатель Аппаратов + + //GUI Names + #LOC_BDArmory_EjectVelocity = Скорость Выброса + #LOC_BDArmory_TNTMass = Тротиловый эквивалент + #LOC_BDArmory_BlastRadius = Радиус Взрыва + #LOC_BDArmory_WeaponName = Название Оружия\u0020 + #LOC_BDArmory_GuidanceType = Тип Наведения\u0020 + #LOC_BDArmory_TargetingMode = Режим Наведения\u0020 + #LOC_BDArmory_ActiveRadarRange = Активный Радарный Диапазон + + #LOC_BDArmory_Rails = Направляющие + #LOC_BDArmory_IncreaseHeight = Высота ++ + #LOC_BDArmory_DecreaseHeight = Высота -- + #LOC_BDArmory_IncreaseLength = Длина ++ + #LOC_BDArmory_DecreaseLength = Длина -- + #LOC_BDArmory_RailsPlus = Кол-во Направляющих ++ + #LOC_BDArmory_RailsMinus = Кол-во Направляющих -- + + #LOC_BDArmory_PilotAI_PID = PID Контроллер + #LOC_BDArmory_SteerFactor = Сила (P) + #LOC_BDArmory_SteerKi = Корректировка (I) + #LOC_BDArmory_SteerDamping = Амортизация (D) + + #LOC_BDArmory_DynamicSteerDamping = Динамическая Амортизация + #LOC_BDArmory_DynamicDamping = Динамическая Амортизация + #LOC_BDArmory_DynamicDampingMin = Амортизация (От Цели) + #LOC_BDArmory_DynamicDampingMax = Амортизация (На Цель) + #LOC_BDArmory_DynamicDampingFactor = Коэфицент Динамической Амортизации + #LOC_BDArmory_3AxisDynamicSteerDamping = Трехосевая Амортизация + #LOC_BDArmory_DynamicDampingPitch = Амортизация Тангажа + #LOC_BDArmory_DynamicDampingPitchMin = Амортизация Тангажа (От Цели) + #LOC_BDArmory_DynamicDampingPitchMax = Амортизация Тангажа (На Цель) + #LOC_BDArmory_DynamicDampingPitchFactor = Коэфицент Дин. Амортизации Тангажа + #LOC_BDArmory_DynamicDampingYaw = Амортизация Рысканья + #LOC_BDArmory_DynamicDampingYawMin = Амортизация Рысканья (От Цели) + #LOC_BDArmory_DynamicDampingYawMax = Амортизация Рысканья (На Цель) + #LOC_BDArmory_DynamicDampingYawFactor = Коэфицент Дин. Амортизации Рысканья + #LOC_BDArmory_DynamicDampingRoll = Амортизация Крена + #LOC_BDArmory_DynamicDampingRollMin = Амортизация Крена (От Цели) + #LOC_BDArmory_DynamicDampingRollMax = Амортизация Крена (На Цель) + #LOC_BDArmory_DynamicDampingRollFactor = Коэфицент Дин. Амортизации Крена + + #LOC_BDArmory_PIDAutoTune = PID Авто-Настройка + #LOC_BDArmory_AutoTuningLoss = Потери Авто-Настройки + #LOC_BDArmory_PIDAutoTuningNumSamples = Кол-во Образцов Авто-Настройки + #LOC_BDArmory_PIDAutoTuningFastResponseRelevance = Важность Быстрого Реагирования Авто-Настройки + #LOC_BDArmory_PIDAutoTuningInitialLearningRate = Исходная Скорость Обучения Авто-Настройки + #LOC_BDArmory_PIDAutoTuningSpeed = Скорость для Авто-Настройки + #LOC_BDArmory_PIDAutoTuningAltitude = Высота для Авто-Настройки + #LOC_BDArmory_PIDAutoTuningFixedP = Фиксированная Сила (P) + #LOC_BDArmory_PIDAutoTuningClampMaximums = Максимальная Фиксация Авто-Настройки + #LOC_BDArmory_AIWindow_PIDAutoTuningFixedFields = Фиксированные Поля А-Н + + #LOC_BDArmory_TargetPriority = Приоритетность Целей + #LOC_BDArmory_TargetPriority_CurrentTarget = Текущая Цель + #LOC_BDArmory_TargetPriority_TargetScore = Счет Цели + #LOC_BDArmory_TargetPriority_Settings = Настройки Приоритетности Цели + #LOC_BDArmory_TargetPriority_CurrentTargetBias = Смена Текущей Цели + #LOC_BDArmory_TargetPriority_TargetProximity = Дистанция до Цели + #LOC_BDArmory_TargetPriority_AirVsGround = Предпочтение Воздушной Цели + #LOC_BDArmory_TargetPriority_CloserAngleToTarget = Меньший Угол до Цели + #LOC_BDArmory_TargetPriority_TargetAcceleration = Ускорение Цели + #LOC_BDArmory_TargetPriority_ShorterClosingTime = Меньшее Время Сближения + #LOC_BDArmory_TargetPriority_TargetWeaponNumber = Кол-во Оружия у Цели + #LOC_BDArmory_TargetPriority_TargetMass = Масса Цели + #LOC_BDArmory_TargetPriority_FewerTeammatesEngaging = Наименьшее кол-во Союзников Атакует + #LOC_BDArmory_TargetPriority_TargetThreat = Угроза Цели + #LOC_BDArmory_TargetPriority_AngleOverDistance = Угол / Расстояние + #LOC_BDArmory_TargetPriority_TargetProtectTeammate = Защищать Союзников + #LOC_BDArmory_TargetPriority_TargetProtectVIP = Защищать VIP Союзников + #LOC_BDArmory_TargetPriority_TargetAttackVIP = Атаковать VIP Цели + + #LOC_BDArmory_Countermeasure_Settings = Настройки Контрмер + #LOC_BDArmory_CMThreshold = Время Пуска Контрмер до Контакта + #LOC_BDArmory_CMRepetition = Кол-во Пусков Вспышек за Цикл + #LOC_BDArmory_CMInterval = Интервал между Пусками Вспышек + #LOC_BDArmory_CMWaitTime = Интервал между Циклами Пуска Вспышек + #LOC_BDArmory_ChaffRepetition = Кол-во Пусков Отражателей за Цикл + #LOC_BDArmory_ChaffInterval = Интервал между Пусками Отражателей + #LOC_BDArmory_ChaffWaitTime = Интервал между Циклами Пуска Вспышек + #LOC_BDArmory_ChaffFactor = Восприимчивость Дипольных Отражателей + + #LOC_BDArmory_IsVIP = VIP + #LOC_BDArmory_IsVIP_enabledText = Да + #LOC_BDArmory_IsVIP_disabledText = Нет + + // Modular Missile, Custom Weapons + #LOC_BDArmory_StagesNumber = Кол-во Ступеней + #LOC_BDArmory_StageToTriggerOnProximity = Детонирующая Ступень + #LOC_BDArmory_RollCorrection = Корректирование Крена + #LOC_BDArmory_RollCorrection_enabledText = Крен вкл. + #LOC_BDArmory_RollCorrection_disabledText = Крен выкл. + #LOC_BDArmory_TimeBetweenStages = Время Между Ступенями + #LOC_BDArmory_MinSpeedGuidance = Мин. Время до Наведения + #LOC_BDArmory_ClearanceRadius = Радиус зазора + #LOC_BDArmory_ClearanceLength = Длина зазора + #LOC_BDArmory_showRFGUI = Показать Редактор Имени Оружия + #LOC_BDArmory_showRFGUI_enabledText = Интерфейс Имени Оружия + #LOC_BDArmory_showRFGUI_disabledText = Интерфейс + + #LOC_BDArmory_PilotAI_Altitudes = Высота Полета + #LOC_BDArmory_DefaultAltitude = Обычная Высота Полета + #LOC_BDArmory_MinAltitude = Мин. Высота Полета + #LOC_BDArmory_MaxAltitude = Макс. Высота Полета (AGL) + + #LOC_BDArmory_PilotAI_Speeds = Скорость + #LOC_BDArmory_MaxSpeed = Макс. Скорость + #LOC_BDArmory_TakeOffSpeed = Взлетная Скорость + #LOC_BDArmory_MinSpeed = Мин. Боевая Скорость + #LOC_BDArmory_StrafingSpeed = Скорость Обстрела + #LOC_BDArmory_IdleSpeed = Крейсерская Скорость + #LOC_BDArmory_ABPriority = Приоритетность Форсажа + + #LOC_BDArmory_MaxDrift = Максимальный Дрифт + + #LOC_BDArmory_PilotAI_ControlLimits = Контрольные Ограничения + #LOC_BDArmory_SteerLimiter = Предел Разворота + #LOC_BDArmory_LowSpeedSteerLimiter = Минимальная Скорость Разворота + #LOC_BDArmory_LowSpeedLimiterSpeed = Минимальный Предел Скорости + #LOC_BDArmory_HighSpeedSteerLimiter = Максимальная Скорость Разворота + #LOC_BDArmory_HighSpeedLimiterSpeed = Максимальный Предел Скорости + #LOC_BDArmory_AltitudeSteerLimiterFactor = Коэф. Предела Высоты Разворота + #LOC_BDArmory_AltitudeSteerLimiterAltitude = Предел Высоты Разворота + #LOC_BDArmory_AttitudeLimiter = Предел Высоты + #LOC_BDArmory_BankLimiter = Предел Угла + #LOC_BDArmory_WaypointPreRollTime = Время Упрежд. Крена (Вейпоинты) + #LOC_BDArmory_WaypointYawAuthorityTime = Время Рысканья (Вейпоинты) + #LOC_BDArmory_maxAllowedGForce = Макс. Перегрузка + #LOC_BDArmory_maxAllowedAoA = Макс. Угол Атаки + + #LOC_BDArmory_PilotAI_EvadeExtend = Уклонение/Увеличение + #LOC_BDArmory_ExtendMultiplier = Увеличить Множитель + #LOC_BDArmory_ExtendDistanceAirToAir = Увеличить Дистанцию Воздух-Воздух + #LOC_BDArmory_ExtendAngleAirToAir = Увеличить Угол Воздух-Воздух + #LOC_BDArmory_ExtendDistanceAirToGroundGuns = Увел. Дистанцию Воздух-Земля (Пушки) + #LOC_BDArmory_ExtendDistanceAirToGround = Увеличить Дистанцию Воздух-Земля + #LOC_BDArmory_ExtendTargetVel = Коэфицент Скорости Цели + #LOC_BDArmory_ExtendTargetAngle = Увеличить Угол до Цели + #LOC_BDArmory_ExtendTargetDist = Увеличить Расстояние до Цели + #LOC_BDArmory_ExtendToggle = Увеличение Расстояния + #LOC_BDArmory_MinEvasionTime = Мин. Время Уклонения + #LOC_BDArmory_EvasionNonlinearity = Нелинейность Уклонения/Увеличения + #LOC_BDArmory_EvasionThreshold = Пороговая Дистанция Уклонения + #LOC_BDArmory_EvasionTimeThreshold = Пороговое Время Уклонения + #LOC_BDArmory_EvasionIgnoreMyTargetTargetingMe = Не Уклоняться от Цели + #LOC_BDArmory_CollisionAvoidanceThreshold = Порог Уклонения Аппарата + #LOC_BDArmory_CollisionAvoidanceLookAheadPeriod = Упреждение Уклонения Аппарата + #LOC_BDArmory_CollisionAvoidanceStrength = Сила Уклонения Аппарата + #LOC_BDArmory_StandoffDistance = Удерживаемая Дистанцию + + #LOC_BDArmory_PilotAI_Terrain = Избежание Крушения об Землю + #LOC_BDArmory_TurnRadiusTwiddleFactorMin = Мин. Избегание Крушения + #LOC_BDArmory_TurnRadiusTwiddleFactorMax = Макс. Избегание Крушения + #LOC_BDArmory_WaypointTerrainAvoidance = Избегание Крушения (Вейпоинты) + + #LOC_BDArmory_PilotAI_Ramming = Таран + #LOC_BDArmory_ControlSurfaceLag = Задержка Управления при Таране + #LOC_BDArmory_AllowRamming = Разрешить Таран + + #LOC_BDArmory_PilotAI_Ejection = Катапультирование + #LOC_BDArmory_EjectOnImpendingDoom = Катапультирование в Критической Ситуации + + #LOC_BDArmory_SliderResolution = Разрешение Ползунков + #LOC_BDArmory_UnclampTuning = Разблокировать настройки\u0020 + #LOC_BDArmory_UnclampTuning_enabledText = Разблокировано + #LOC_BDArmory_UnclampTuning_disabledText = Заблокировано + + //Pilot AI GUI context labels + #LOC_BDArmory_AIWindow_title = ИИ-контроллер + #LOC_BDArmory_AIWindow_infoLink = Инфолинк + #LOC_BDArmory_AIWindow_NoAI = ИИ-контроллер не найден. + #LOC_BDArmory_PilotAI_Misc = Разное + #LOC_BDArmory_AIWindow_SteerMultLow = <- Вялый + #LOC_BDArmory_AIWindow_SteerMultHi = Резкий -> + #LOC_BDArmory_AIWindow_SteerKiLow = <- Недолет + #LOC_BDArmory_AIWindow_SteerKiHi = Перелет -> + #LOC_BDArmory_AIWindow_SteerDampLow = <- Шаткий + #LOC_BDArmory_AIWindow_SteerDampHi = Стабильный -> + #LOC_BDArmory_AIWindow_DynDampMin = Минимальная Амортизация + #LOC_BDArmory_AIWindow_DynDampMax = Максимальная Амортизация + #LOC_BDArmory_AIWindow_DynDampMult = Величина Амортизации + #LOC_BDArmory_AIWindow_PIDAutoTuningNumSamplesMin = <- Менее точный + #LOC_BDArmory_AIWindow_PIDAutoTuningNumSamplesMax = Более точный -> + #LOC_BDArmory_AIWindow_PIDAutoTuningFastResponseRelevanceMin = <- Сильная амортизация + #LOC_BDArmory_AIWindow_PIDAutoTuningFastResponseRelevanceMax = Быстрое реагирование -> + #LOC_BDArmory_AIWindow_PIDAutoTuningInitialLearningRateContext = Понизить, если изменения значений в PID слишком большие + #LOC_BDArmory_AIWindow_PIDAutoTuningAltitudeContext = Старайться оставаться в пределах этой высоты + #LOC_BDArmory_AIWindow_PIDAutoTuningSpeedContext = Целевая скорость авто-настройки + #LOC_BDArmory_AIWindow_DefAlt = ИИ возвращается к этому при бездействии + #LOC_BDArmory_AIWindow_MinAlt = ИИ пытается оставаться над этим + #LOC_BDArmory_AIWindow_MaxAlt = ИИ пытается оставаться под этим + #LOC_BDArmory_AIWindow_maxSpeed = ИИ остается ниже этой скорости полета + #LOC_BDArmory_AIWindow_takeoff = Скорость вхождения ИИ в тангаж + #LOC_BDArmory_AIWindow_minSpeed = ИИ будет удаляться ниже этого + #LOC_BDArmory_AIWindow_atkSpeed = Скорость атаки наземных целей + #LOC_BDArmory_AIWindow_idleSpeed = Крейсерская скорость + #LOC_BDArmory_AIWindow_ABPriority = Приоритет включения форсажа + #LOC_BDArmory_AIWindow_LSSL = Ограничивает контроль ниже наименьшего лимита скорости + #LOC_BDArmory_AIWindow_LSLS = ИИ использует минимальный лимит скорости ниже этого + #LOC_BDArmory_AIWindow_HSSL = Ограничивает контроль выше наименьшего лимита скорости + #LOC_BDArmory_AIWindow_HSLS = ИИ полностью ограничен максимальным пределом высоты + #LOC_BDArmory_AIWindow_ASLF = Коэфицент доя уменьшения/увеличения лимита разворота, основываясь на высоте + #LOC_BDArmory_AIWindow_ASLA = Высота для начала уменьшения/увеличения лимита разворота + #LOC_BDArmory_AIWindow_WPPreRoll = Начать крен заранее до достижения вейпоинта + #LOC_BDArmory_AIWindow_WPYawAuth = Увеличить отклик рысканья во время приближения к вейпоинту + #LOC_BDArmory_AIWindow_bankLimit = Макс. угол крена + #LOC_BDArmory_AIWindow_GForce = Перегрузка не будет превышать это значение + #LOC_BDArmory_AIWindow_AoA = Угол атаки не будет превышать это значение + #LOC_BDArmory_AIWindow_MinEvade = Минимальное время уклонения ИИ от атаки + #LOC_BDArmory_AIWindow_EvExNonlin = Сила колебаний во время уклонения/увеличения расстояния + #LOC_BDArmory_AIWindow_evadeDist = Избегает вражеского огня внутри этого + #LOC_BDArmory_AIWindow_evadetimeDist = Минимальное время обстрела для активации уклонения + #LOC_BDArmory_AIWindow_ColDist = Уклоняться от вражеского аппарата на этой дистанции + #LOC_BDArmory_AIWindow_ColTime = Как далеко ИИ упреждает столкновение + #LOC_BDArmory_AIWindow_standoff = Минимальная дистанция, на которую ИИ будет приближаться к цели + #LOC_BDArmory_AIWindow_ExtendAngleAirToAir_Context = Желаемый угол подъема при увеличении + #LOC_BDArmory_AIWindow_ExtendDistanceAirToAir_Context = Увеличить Угол (Воздух-Воздух) + #LOC_BDArmory_AIWindow_ExtendDistanceAirToGroundGuns_Context = Увеличить расстояние до земли (Пулеметы) + #LOC_BDArmory_AIWindow_ExtendDistanceAirToGround_Context = Увеличить Дистанцию Воздух-Земля + #LOC_BDArmory_AIWindow_Extendvel = Устанавливает, когда цель слишком медленна, чтобы поворачивать к ней + #LOC_BDArmory_AIWindow_ExtendAngle = Устанавливает, когда цель вне радиуса поворота + #LOC_BDArmory_AIWindow_ExtendDist = Устанавливает, когда цель слишком близко, чтобы поворачивать к ней + #LOC_BDArmory_AIWindow_terrainMin = Множитель радиусса разворота для идеальной ориентации аппарата + #LOC_BDArmory_AIWindow_terrainMax = Множитель радиусса разворота для обратной ориентации аппарата + #LOC_BDArmory_AIWindow_WaypointTerrainAvoidanceContext = Расстояние и сила корректировки избежания крушения в режиме вейпоинтов + #LOC_BDArmory_AIWindow_ramLag = Корректировка траектории тарана для задержки элевонов + #LOC_BDArmory_AIWindow_orbit = Направление курсирования + #LOC_BDArmory_AIWindow_standby = ИИ включается, когда цель входит в боевой радиус + + //PilotAi GUI labels + #LOC_BDArmory_AIWindow_PIDAutoTuningNumSamples = Кол-во Примеров + #LOC_BDArmory_AIWindow_PIDAutoTuningFastResponseRelevance = Скор. Отклика + #LOC_BDArmory_AIWindow_PIDAutoTuningInitialLearningRate = Изначальная скорость обучения + #LOC_BDArmory_AIWindow_PIDAutoTuningSpeed = Скорость + #LOC_BDArmory_AIWindow_PIDAutoTuningAltitude = Высота + #LOC_BDArmory_AIWindow_PIDAutoTuningFixedP = Неизм. Сила + #LOC_BDArmory_AIWindow_PIDAutoTuningClampMaximums = Макс. Зажим + #LOC_BDArmory_AIWindow_ControlLimits = Контроль + #LOC_BDArmory_AIWindow_SteerLimiter = Лимит Поворота + #LOC_BDArmory_AIWindow_LowSpeedSteerLimiter = Наим. Предел Скорости Поворота + #LOC_BDArmory_AIWindow_LowSpeedLimiterSpeed = Наим. Предел Скорости + #LOC_BDArmory_AIWindow_HighSpeedSteerLimiter = Наиб. Предел Скорости Поворота + #LOC_BDArmory_AIWindow_HighSpeedLimiterSpeed = Наиб. Предел Скорости + #LOC_BDArmory_AIWindow_AltitudeSteerLimiterFactor = Фактор Высоты Поворота + #LOC_BDArmory_AIWindow_AltitudeSteerLimiterAltitude = Лимит Высоты Поворота + #LOC_BDArmory_AIWindow_WaypointPreRollTime = Время Упрежд. Крена (ВП) + #LOC_BDArmory_AIWindow_WaypointYawAuthorityTime = Время Рысканья (ВП) + #LOC_BDArmory_AIWindow_EvadeExtend = Энергия + #LOC_BDArmory_AIWindow_ExtendDistanceAirToAir = Увел. Дист. В-В + #LOC_BDArmory_AIWindow_ExtendAngleAirToAir = Увел. Угол + #LOC_BDArmory_AIWindow_ExtendDistanceAirToGroundGuns = Увел. Дист. В-З (Пушки) + #LOC_BDArmory_AIWindow_ExtendDistanceAirToGround = Увел. Дист. В-З + #LOC_BDArmory_AIWindow_ExtendTargetVel = Коэф. Скорости Цели + #LOC_BDArmory_AIWindow_ExtendTargetAngle = Угол до Цели + #LOC_BDArmory_AIWindow_ExtendTargetDist = Дист. до Цели + #LOC_BDArmory_AIWindow_EvasionNonlinearity = Нелинейность Уклон./Увелич. + #LOC_BDArmory_AIWindow_EvasionThreshold = Дист. Уклонения + #LOC_BDArmory_AIWindow_EvasionTimeThreshold = Порог Уклонения + #LOC_BDArmory_AIWindow_CollisionAvoidanceThreshold = Дист. Уклонения Аппарата + #LOC_BDArmory_AIWindow_CollisionAvoidanceLookAheadPeriod = Упрежд. Уклонение Аппарата + #LOC_BDArmory_AIWindow_CollisionAvoidanceStrength = Сила Уклонения Аппарата + #LOC_BDArmory_AIWindow_StandoffDistance = Держать Дистанцию + #LOC_BDArmory_AIWindow_Terrain = Земля + #LOC_BDArmory_AIWindow_TurnRadiusMin = Мин. Уклонение у Земли + #LOC_BDArmory_AIWindow_TurnRadiusMax = Макс. Уклонение у Земли + #LOC_BDArmory_AIWindow_WaypointTerrainAvoidance = Уклонение у Земли (Вейпоинты) + #LOC_BDArmory_AIWindow_ControlSurfaceLag = Задержка Элевонов + + //Pilot AI infolink + #LOC_BDArmory_AIWindow_PidHelp = The PID (Proportional Integral Derivative) Controller calculates the difference between desired and actual output, then applies corrections based on P, I, and D values. This is used to steer the craft. Tuning generally requires adjusting all three values, starting with Steer Mult, then adjusting Ki and Damping as needed. + #LOC_BDArmory_AIWindow_PidHelp_SteerMult = Steer Power (P) - This is the control input. Too low, and the craft will use less than the full range of its control authority. Too high, and the craft will apply more than needed and overshoot the desired orientation. + #LOC_BDArmory_AIWindow_PidHelp_SteerKi = Steer Ki (I) - This is the error correction applied fix accumulated error from P and D. Too little, and the craft will consistently undershoot its desired orientation. Too much, and it will overshoot. + #LOC_BDArmory_AIWindow_PidHelp_Steerdamp = Steer Damp (D) - This is the derivative value; this is how much the craft's rotation to a new orientation will be damped out. Too low, and the craft will oscillate due to overshooting the correct orientation. Too high, and the damping will counter too much of the change in orientation, and craft will not turn as fast. + #LOC_BDArmory_AIWindow_PidHelp_Dyndamp = Dynamic Damping - This dynamically adjusts Damping, from the min damping value to the max damping value, based on angle to target. The lower the value, the more linear the dynamic damping value as target angle changes, the higher the value, the more damping will be reduced when pointing away from a target, and increased when pointing near a target. This applies to all three control axes. For individual control Axis damping, enable the relevant Pitch/Roll/Yaw Dynamic Damping. + #LOC_BDArmory_AIWindow_PidHelp_AutoTune = PID Auto-Tune - This enables an automated PID tuning mode where the AI will use gradient descent to optimize the plane's ability to turn to a range of headings and stabilize in those directions. + #LOC_BDArmory_AIWindow_PidHelp_AutoTune_details = \n - The loss being minimized is ∫f(x,θ)dθ over the range θ ∈ (30°,120°) (using the midpoint Riemann sum), where f(x,θ) is\n ∫(δp²·(α+t²)/θ² + γ·δr²·(α+t)/100/θ)dt\nfor the current PID values (x) and heading change (θ), where δp is the pointing error, δr is the roll error, α is the fast response relevance and γ is the roll relevance (which is automatically adjusted over time to balance the contribution from the pointing and roll errors).\n - Usage: Once the plane is airborne (and not in combat), enable auto-tuning and set the sliders to the desired values (the defaults are reasonable starting points and can be preset in the SPH; adjusting some of the sliders will restart the auto-tuning), then allow the auto-tuning to run until it stops automatically when the learning rate (LR) decreases to below 1e-3. The PID values will revert to those giving the lowest loss and these will be stored so they can be restored in the SPH.\n - Parameters: Num Samples - The number of heading changes (θ) used in the Riemann sum; higher values will decrease noise in the gradient. Fast Response (α) - How much to weight the early pointing and roll errors. Initial LR - The initial learning rate. Altitude and Speed - The target altitude and speed to use while tuning. Fixed P - Keep P fixed and only tune the other fields. Clamp Max - Keep the tuned values within the limits of the sliders.\n - Recommendations: 1. Set the auto-tuning altitude and speed to those expected to be used in combat. 2. Use 5-10x time-scaling. 3. Avoid mountainous terrain. 4. Tune without dynamic damping first and use the result as the starting point for dynamic damping with all the damping values set to the tuned static damping value and the dynamic damping factors set to 1. 5. Since the PID values are (currently) being optimized for flying to fixed points, the tuned I value may not be optimal for moving targets in combat and a slightly larger I may be desirable. + + #LOC_BDArmory_AIWindow_AltHelp = Altitude settings control the desired flight envelope of the AI + #LOC_BDArmory_AIWindow_AltHelp_Def = Default Altitude - This is the Altitude the AI will seek to return to when not in combat or extending. + #LOC_BDArmory_AIWindow_AltHelp_min = Minimum Altitude - This sets what altitude the craft must descend past before the AI will begin climbing; Make sure to leave enough room for the craft to pull up. + #LOC_BDArmory_AIWindow_AltHelp_max = Maximum Altitude - If enabled, this is the inverse of Min Alt; this sets what altitude the craft must exceed before the AI starts diving. + + #LOC_BDArmory_AIWindow_SpeedHelp = Speed settings control the desired Airspeeds of the craft over a variety of flight and combat conditions + #LOC_BDArmory_AIWindow_SpeedHelp_min = Min and Max speed - Max speed is the airspeed the AI will attempt to reach, but not exceed, when in combat or ramming. If above this speed, the AI will brake until it is below the Max Speed. Min Speed is the minimum speed the AI will perform combat maneuvers, regardless of target speed. If below this speed, the AI will break and extend to accelerate until it is above the Min Speed. + #LOC_BDArmory_AIWindow_SpeedHelp_takeoff = Takeoff Speed - If the craft is landed when the AI is activated, Takeoff Speed is the speed the craft must reach before the AI will begin to pitch up to takeoff. + #LOC_BDArmory_AIWindow_SpeedHelp_idle = Idle Speed - This sets the non-combat cruise airspeed the AI will maintain while orbiting or flying to position. + #LOC_BDArmory_AIWindow_SpeedHelp_gnd = Strafing Speed - This is the airspeed the AI will use when attacking ground targets. If the ground target is moving, Strafing Speed will add the ground target's speed. + #LOC_BDArmory_AIWindow_SpeedHelp_ABpriority = AB Priority - This controls the level of requested acceleration at which the AI will turn on/off the afterburners. + + #LOC_BDArmory_AIWindow_ControlHelp = Control Limits set limits on craft control authority under various conditions + #LOC_BDArmory_AIWindow_ControlHelp_limiters = Steer Limiters - Steer Limiters limit Control Authority of the craft. The Low Speed Limiter sets AI control authority when at or below the Low-Speed Limit Speed. The High-Speed Limiter sets AI control authority when at or above the High-Speed Limit Speed. When between the Low and High Limit Speeds, the Limiter value linearly changes from the Low Limit to the High Limit value. The Altitude Steer Limiter scales the steer limit based on altitude above the limit by (altitude/limit)^factor. + #LOC_BDArmory_AIWindow_ControlHelp_bank = Max Bank - This sets the max allowed bank angle. When below 180, the AI will not roll past this many degrees from Horizontal during maneuvers. + #LOC_BDArmory_AIWindow_ControlHelp_clamps = Max Allowed G and AoA - Max Allowed G limits the AI to maneuvers that pull this many Gs or less. Max Allowed AoA limits the the maximum Angle of Attack the AI can use. + + #LOC_BDArmory_AIWindow_EvadeHelp = Evasion/Extension controls both how the AI reacts to incoming threats, be it gunfire, missiles, or other craft, and how the AI responds to where other craft are, relative to itself. + #LOC_BDArmory_AIWindow_EvadeHelp_Evade = EvasionTime, EvasionThreshold, EvasionTimeThreshold, Don't Evade My Target - These four settings control when the AI will evade. EvasionTime sets how many seconds the AI will do evasive maneuvers. EvasionTimeThreshold sets how long the AI needs to be under fire before it begins evading. EvasionThreshold sets how close incoming gunfire needs to come to trigger evasion. The Don't Evade My Target toggle determines whether gunfire from the current target is ignored for evasion purposes. + #LOC_BDArmory_AIWindow_EvadeHelp_Dodge = Vessel Avoidance - These three settings set how the AI reacts to potential collisions. If another vessel is predicted to get within the Avoidance Threshold within the next Look-Ahead period then the AI will try to dodge it. The Avoidance Strength determines how rapidly the AI will try to change direction to avoid a predicted collision. + #LOC_BDArmory_AIWindow_EvadeHelp_standoff = Standoff Distance is the closest the AI will approach the targeted craft in combat. If closer than the Standoff Distance, the AI will brake to increase the distance to the target. + #LOC_BDArmory_AIWindow_EvadeHelp_Extend = Extend Distances - These settings control how far the AI will extend against various types of targets. The extend angle controls whether the AI should try to gain or lose altitude when extending against air targets. + #LOC_BDArmory_AIWindow_EvadeHelp_ExtendVars = Target Angle, Target Dist, Extend Target Vel - These three settings together control when the AI will extend. To extend, the AI projects a detection cone ahead of it, checks the distance to the target, and the relative velocity. By default, the AI will extend if the target is outside a 78 degree cone ahead of the AI, closer than 400m, and has a slower airspeed. + #LOC_BDArmory_AIWindow_EvadeHelp_ExtendAngle = Extend Target Angle - This sets the width of the detection cone, and can be though of as a combination field of view and effective turn radius. The better the craft's turn radius, the higher this value can be. + #LOC_BDArmory_AIWindow_EvadeHelp_ExtendDist = Extend Target Distance - This sets how close the target has to be before the AI will extend to get a better angle on the target. + #LOC_BDArmory_AIWindow_EvadeHelp_ExtendVel = Extend Target Velocity - This tells the AI at what relative velocity it should consider extending. Less than 1, the target craft must be slower, greater than 1, faster. + #LOC_BDArmory_AIWindow_EvadeHelp_Nonlinearity = Evasion/Extension Nonlinearity - This controls the radius of the oscillation (in degrees) around the fly-to direction that the plane will make while evading or extending. This helps plane not fly in a straight line when evading/extending. + #LOC_BDArmory_AIWindow_EvadeHelp_ExtendToggle = Extend Toggle - This enables or disables extending against air targets (extending against ground targets is not affected). + + #LOC_BDArmory_AIWindow_TerrainHelp = Terrain Avoidance is used to predict collisions with the ground, by scaling the craft's turn radius to generate a terrain collision distance to tell the AI if it needs to pull up. The Terrain Avoid Min value is based on optimum flight conditions, with the plane parallel to the ground and only needs to pitch up. Terrain Avoid Max is based on the worst case scenerio with the plane inverted to the ground, and needs to roll 180 degrees before pitching up. Waypoint Terrain Avoidance affects the range and strength of the adjustment to the fly-to direction due to terrain being between the craft and the current waypoint. + + #LOC_BDArmory_AIWindow_RamHelp = Ramming controls if the craft should attempt to ram other craft when it is out of ammo or no longer has working weapons. If ramming is not enabled, the AI will instead continue to maneuver, but be unable to engage. Control Surface lag sets how much the AI should correct collision predictions, based on how long control surfaces take to reach full deflection. + + #LOC_BDArmory_AIWindow_miscHelp = Misc Settings - These control non-combat behaviors that don't fit elsewhere. + #LOC_BDArmory_AIWindow_orbitHelp = Orbit Direction - This is the direction the AI will orbit when idling, either Clockwise or Counterclockwise. + #LOC_BDArmory_AIWindow_standbyHelp = Standby Toggle - If enabled, the AI will automatically turn on when targets enter its Guard Range. + + // Idle Behavior + #LOC_BDArmory_Orbit = Направление по орбите \u0020 + #LOC_BDArmory_Orbit_Starboard = Штирборт (CW) + #LOC_BDArmory_Orbit_Port = Порт (CCW) + #LOC_BDArmory_Orbit_Random = Любой (CW/CCW) + #LOC_BDArmory_StandbyMode = Режим Ожидания + + #LOC_BDArmory_On = Вкл. + #LOC_BDArmory_Off = Откл. + + #LOC_BDArmory_VehicleType = Тип Аппарата + #LOC_BDArmory_MaxSlopeAngle = Макс. Угол Уклона + #LOC_BDArmory_CruiseSpeed = Крейсерская Скорость + #LOC_BDArmory_CombatSpeed = Боевая Скорость + #LOC_BDArmory_CombatAltitude = Боевая Высота + #LOC_BDArmory_TargetPitch = Moving Pitch + #LOC_BDArmory_MaxPitchAngle = Max Pitch Angle + #LOC_BDArmory_BankAngle = Bank Angle + #LOC_BDArmory_MaxBankAngle = Max Bank Angle + #LOC_BDArmory_BroadsideAttack = Вектор Атаки + #LOC_BDArmory_BroadsideAttack_enabledText = Прямой + #LOC_BDArmory_BroadsideAttack_disabledText = Парабола + #LOC_BDArmory_MinEngagementRange = Мин. Дистанция Атаки + #LOC_BDArmory_MaxEngagementRange = Макс. Дистанция Атаки + #LOC_BDArmory_ManeuverRCS = РСУ Активна + #LOC_BDArmory_ManeuverRCS_enabledText = Маневры + #LOC_BDArmory_ManeuverRCS_disabledText = Сражение + #LOC_BDArmory_MinObstacleMass = Мин. Масса Препятствия + #LOC_BDArmory_PreferredBroadsideDirection = Предпочтительное Направление + #LOC_BDArmory_GoesUp = Макс. Значения + #LOC_BDArmory_GoesUp_enabledText = 11 + #LOC_BDArmory_GoesUp_disabledText = 10 + + //Surface AI GUI context labels + #LOC_BDArmory_DriverAI_VeeType = Аппарат действует на этом типе рельефа + #LOC_BDArmory_DriverAI_SlopeAngle = Максимальный наклон поверхности, по которой аппарат будет ехать вверх + #LOC_BDArmory_DriverAI_MaxSpeed = Максимальная скорость в бою + #LOC_BDArmory_DriverAI_MaxDrift = Max angle craft veers off prograde during cornering + #LOC_BDArmory_DriverAI_Pitch = Desired craft Attitude Angle + #LOC_BDArmory_DriverAI_SteerMult = Контролирует установленный множитель + #LOC_BDArmory_DriverAI_AtkVector = Ориентация аппарата относительно цели во время атаки + #LOC_BDArmory_DriverAI_MinEngage = Мин. дальность атаки ИИ + #LOC_BDArmory_DriverAI_MaxEngage = Макс. дальность атаки ИИ + #LOC_BDArmory_DriverAI_RCS = Условия использования РСУ + #LOC_BDArmory_DriverAI_Mass = Минимальная масса объекта, от которого необходимо уклоняться + #LOC_BDArmory_DriverAI_BradsideDir = Сторона аппарата, которая должна смотреть на цель + + //Surface AI GUI infolink + #LOC_BDArmory_DriverAI_Help = Vehicle Type - This tells the AI if the vessel is a land vehicle, a ship, or an amphibious craft capable of both land and water operation. + #LOC_BDArmory_DriverAI_Slopes = Slope Angle and target Pitch - These set angle constraints for the vessel. Slope Angle sets the max slope the AI will attempt to drive up. Target Pitch sets the desired vessel Attitude (Pitch angle relative to the ground) the vessel should try to maintain. + #LOC_BDArmory_DriverAI_Speeds = Cruise and Max speed set vessel speeds. Cruise Speed is the speed the AI will seek to maintain while not in combat. max Speed is the speed the AI will attempt to reach when in combat. + #LOC_BDArmory_DriverAI_Drift = Max Drift - This sets the maximum the vessel will veer off prograde when turing. + #LOC_BDArmory_DriverAI_bank - Bank Angle - This tells the AI the maximum it should allow the vessel to bank or heel over during maneuvers. + #LOC_BDArmory_DriverAI_steerMult = Steer Mult - This is the Proportional input for the Surface AI PID controller. Too low, and the craft will use less than the full range of its control authority. Too high, and the craft will apply more than needed and overshoot the desired orientation. + #LOC_BDArmory_DriverAI_SteerDamp = Steer Damp - This is the Derivative input for the Surface AI PID controller. This is how much the craft's rotation to a new orientation will be damped out. Too low, and the craft will oscillate due to overshooting the correct orientation. Too high, and the damping will counter too much of the change in orientation, and craft will not turn as fast. + #LOC_BDArmory_DriverAI_Orientation = Attack Vector, Broadside Dir - These set how the AI will approach and engage targets. The Attack Vector setting tells the AI either to engage with the bow of the vessel pointing at the target, or if to have the vessel's broadside pointing at the target. Broadside Direction tells the AI whether it should favor presenting the Port or Starboard broadside towards the target. + #LOC_BDArmory_DriverAI_Engagement = Min, Max Engagement Range - These set the minimum and maximum distances from the target the AI will attempt to maneuver to be within in order to engage. + #LOC_BDArmory_DriverAI_RCS = RCS - The RCS setting toggles whether the AI should use RCS thrusters to assist in maneuvering. + #LOC_BDArmory_DriverAI_Mass = Mass Avoidance - This sets the minimum mass that an obstace needs to be in order to avoid it instead of ramming it. + + #LOC_BDArmory_DeployAltitude = Высота Развертывания + #LOC_BDArmory_EngageRangeMin = Мин. Дистанция Атаки + #LOC_BDArmory_EngageRangeMax = Макс. Дистанция Атаки + #LOC_BDArmory_EngageAir = Атаковать Воздушные Цели + #LOC_BDArmory_EngageMissile = Атаковать Ракеты + #LOC_BDArmory_EngageSurface = Атаковать Наземные Цели + #LOC_BDArmory_EngageSLW = Атаковать Торпеды + #LOC_BDArmory_DisableEngageOptions = Откл. Настройки Атаки + #LOC_BDArmory_EnableEngageOptions = Вкл. Настройки Атаки + #LOC_BDArmory_MaxStaticLaunchRange = Макс. Статичная Дальность Запуска + #LOC_BDArmory_MinStaticLaunchRange = Мин. Статичная Дальность Запуска + #LOC_BDArmory_MaxOffBoresight = Макс. Отклонение + #LOC_BDArmory_DetonationDistanceOverride = Дист. Детонации + #LOC_BDArmory_DetonateAtMinimumDistance = Детонировать на Мин. Дистанции + #LOC_BDArmory_ProximityTriggerDistance = Дист. Детонации Боеголовки + #LOC_BDArmory_DropTime = Время Сбрасывания + #LOC_BDArmory_InCargoBay = В Грузовом Отсеке:\u0020 + #LOC_BDArmory_InCustomCargoBay = Грузовой Отсек:\u0020 + #LOC_BDArmory_DeployableWeapon = Развертываемое Оружие:\u0020 + #LOC_BDArmory_DetonationTime = Время Детонации + #LOC_BDArmory_BallisticOvershootFactor = Коэф. Баллист. Перелета + #LOC_BDArmory_BallisticAnglePath = Баллист. Угловая Траектория + #LOC_BDArmory_CruiseAltitude = Крейсерская Высота + #LOC_BDArmory_CruisePredictionTime = Крейсерское время упреждения + #LOC_BDArmory_GPSTarget = GPS Цель + #LOC_BDArmory_ChangetoLowAltitudeRange = Изменить Дальность на Макс. Высоте + #LOC_BDArmory_MaxAltitude = Макс. Высота + #LOC_BDArmory_TerminalGuidance = Терминальное Наведение:\u0020 + #LOC_BDArmory_Direction = Направление:\u0020 + #LOC_BDArmory_Direction_disabledText = Любое + #LOC_BDArmory_Direction_enabledText = Вперед + #LOC_BDArmory_Ordinance = Скорость Отбрасывания + + #LOC_BDArmory_EMPBlastRadius = Радиус ЭМИ Взрыва + #LOC_BDArmory_OrdinanceAvailable = Доступная Скорость Отбрасывания + #LOC_BDArmory_MissileAssign = Назначение Ракеты + #LOC_BDArmory_CurrentLocks = Текущие Цели + + #LOC_BDArmory_SSTank = Самозапечатываемый Бак + #LOC_BDArmory_SSTank_On = Вкл. Самозапечатываемый Бак + #LOC_BDArmory_SSTank_Off = Выкл. Самозапечатываемый Бак + #LOC_BDArmory_CASE = C.A.S.E. Tier + #LOC_BDArmory_FireBottles = Огнетушители + #LOC_BDArmory_FB_Remaining = Огнетушителей Осталось + #LOC_BDArmory_FIS = Система Инертизации Топлива + #LOC_BDArmory_FIS_On = Вкл. Систему Инертизации Топлива + #LOC_BDArmory_FIS_Off = Выкл. Систему Инертизации Топлива + #LOC_BDArmory_Armorcockpit_On = Вкл. Бронированую Кабину + #LOC_BDArmory_Armorcockpit_Off = Выкл. Бронированую Кабину + + #LOC_BDArmory_MaxPitch = Макс. Тангаж + #LOC_BDArmory_MinPitch = Мин. Тангаж + #LOC_BDArmory_YawRange = Диапазон Рысканья + #LOC_BDArmory_FireLimits = Лимиты Огня + #LOC_BDArmory_FireLimits_disabledText = Нет + #LOC_BDArmory_FireLimits_enabledText = В радиусе + #LOC_BDArmory_DefaultDetonationRange = Дальность Детонации\u0020 + #LOC_BDArmory_ProximityFuzeRadius = Дальность Радиовзрывателя + #LOC_BDArmory_MaxDetonationRange = Макс. Дальность Детонации + #LOC_BDArmory_Barrage = Заградительный Огонь + #LOC_BDArmory_ToggleBarrage = Заградительный Огонь + #LOC_BDArmory_ReturnTurret = Вернуть Турель + #LOC_BDArmory_ToggleAnimation = Анимация + + #LOC_BDArmory_NextTankSetup = След. наполнение бака + #LOC_BDArmory_PreviousTankSetup = Пред. наполнение бака + #LOC_BDArmory_NextTexture = След. Текстура + + #LOC_BDArmory_FireMissile = Запустить Ракету + #LOC_BDArmory_Detonate = Сдетонировать + #LOC_BDArmory_Resupply = Пополнить + #LOC_BDArmory_GuidanceMode = Режим Наведения + #LOC_BDArmory_Jettison = Сбросить + #LOC_BDArmory_ToggleTurret = Зафиксировать Турель + #LOC_BDArmory_TurretEnabled = Откл. Турель + #LOC_BDArmory_AutoReturn = Авто-Возврат + #LOC_BDArmory_MissileTurretFireFOV = Поле Зрения + #LOC_BDArmory_HideUI = Скрыть Именной Интерфейс + #LOC_BDArmory_ShowUI = Показать Именной Интерфейс + #LOC_BDArmory_HideWeaponGroupUI = Скрыть Групповой Интерфейс + #LOC_BDArmory_SetWeaponGroupUI = Показать Групповой Интерфейс + #LOC_BDArmory_Fire = Огонь + #LOC_BDArmory_ToggleRadar = Радар + #LOC_BDArmory_ToggleIRST = Инфракрасный Датчик + + #LOC_BDArmory_GuardMode = Боевой Режим:\u0020 + #LOC_BDArmory_Team = Команда + #LOC_BDArmory_Weapon = Оружие + #LOC_BDArmory_FiringInterval = Огневой Интервал + #LOC_BDArmory_FiringBurstLength = Длина Очереди (Время) + #LOC_BDArmory_FiringBurstCount = Длина Очереди (Кол-во) + #LOC_BDArmory_FiringTolerance = Огневой Угол + #LOC_BDArmory_FieldOfView = Поле Зрения + #LOC_BDArmory_VisualRange = Визуальная Дистанция + #LOC_BDArmory_GunsRange = Дальность Пулеметов + #LOC_BDArmory_MissilesORTarget = Ракеты/Цель + #LOC_BDArmory_FireAngleOverride_Enable = Вкл. Коррекцию Огневого Угла + #LOC_BDArmory_FireAngleOverride_Disable = Вкл. Коррекцию Огневого Угла + #LOC_BDArmory_BurstLengthOverride_Enable = Вкл. Коррекцию Длины Очереди + #LOC_BDArmory_BurstLengthOverride_Disable = Выкл. Коррекцию Длины Очереди + #LOC_BDArmory_FiringAngle = Огневой Угол + + #LOC_BDArmory_false = Ложь + #LOC_BDArmory_true = Истина + + #LOC_BDArmory_AddedCost = Доп. стоимость + #LOC_BDArmory_AddedMass = Масса Системы Безопасности + #LOC_BDArmory_DryMass = Сухая Масса + #LOC_BDArmory_Enabled = Вкл. + #LOC_BDArmory_Disabled = Выкл. + #LOC_BDArmory_Enable = Вкл. + #LOC_BDArmory_Disable = Выкл. + + #LOC_BDArmory_Status = Статус + #LOC_BDArmory_Toggle = Перекл. + #LOC_BDArmory_ShowGroupEditor = Редактор Групп + #LOC_BDArmory_ShowGroupEditor_enabledText = выкл. Групповой Интерфейс + #LOC_BDArmory_ShowGroupEditor_disabledText = вкл. Групповой Интерфейс + #LOC_BDArmory_DeactivationDepth = Глубина Отключения + #LOC_BDArmory_Hitpoints = Здоровье + #LOC_BDArmory_FireCountermeasure = Выпустить Ловушку + + #LOC_BDArmory_TogglePilot = Пилот + #LOC_BDArmory_DeactivatePilot = Откл. Пилот + #LOC_BDArmory_ActivatePilot = Вкл. Пилот + + #LOC_BDArmory_SelectTeam = Выбрать Команду + #LOC_BDArmory_OpenGUI = Открыть Интерфейс + + #LOC_BDArmory_StoreSettings = Копировать Настройки + #LOC_BDArmory_RestoreSettings = Вставить Настройки + #LOC_BDArmory_ControlSurfaceSettings = Настройки УП + #LOC_BDArmory_StoreControlSurfaceSettings = Копировать Настройки УП + #LOC_BDArmory_RestoreControlSurfaceSettings = Вставить Настройки УП + + //ammo switch + #LOC_BDArmory_Ammo_Type = Тип Боеприпасов + #LOC_BDArmory_Ammo_LoadedAmmo = Боеприпасы + #LOC_BDArmory_Ammo_Slug = Металл + #LOC_BDArmory_Ammo_Shot = Кластерные + #LOC_BDArmory_Ammo_AP = Бронебойные + #LOC_BDArmory_Ammo_SAP = Полубронебойный + #LOC_BDArmory_Ammo_Flak = Сближение + #LOC_BDArmory_Ammo_Explosive = Взрывные + #LOC_BDArmory_Ammo_HE = Фугасные + #LOC_BDArmory_Ammo_Shaped = Shaped Charge + #LOC_BDArmory_Ammo_Kinetic = Кинетические + #LOC_BDArmory_Ammo_EMP = Э.М.И. + #LOC_BDArmory_Ammo_Choker = Дымовые + #LOC_BDArmory_Ammo_Impulse = Импульсные + #LOC_BDArmory_Ammo_Gravitic = Гравитационные + #LOC_BDArmory_Ammo_Incendiary = Зажигательные + #LOC_BDArmory_Ammo_Nuclear = Ядерные + #LOC_BDArmory_Ammo_Beehive = Рой + + //Team Icons + #LOC_BDArmory_Icons_title = Значки Командного Интерфейса + #LOC_BDArmory_Icons_PSA = Press F4 to toggle stock vessel icons + #LOC_BDArmory_Enable_Icons = Командные Иконки + #LOC_BDArmory_Icon_show_self = Показать Личность + #LOC_BDArmory_Icon_teams = Вкл. Ярлыки Команд + #LOC_BDArmory_Icon_names = Вкл. Ярлыки Аппаратов + #LOC_BDArmory_Icon_score = Вкл. Счет + #LOC_BDArmory_Icon_healthbars = Уровень Здоровья + #LOC_BDArmory_Icon_missiles = Значки Ракет + #LOC_BDArmory_Icon_debris = Значки Обломков + #LOC_BDArmory_Icon_persist = Не прятать с интерфейсом + #LOC_BDArmory_Icon_threats = Индикатор Угрозы + #LOC_BDArmory_Icon_pointers = Указатели на Значки + #LOC_BDArmory_Icon_scale = Размеры Значков: + #LOC_BDArmory_Icon_distance_threshold = Пороговая Дистанция: + #LOC_BDArmory_Icon_telemetry = Вкл. Телеметрию; + #LOC_BDArmory_Icon_colorget = Применить + + //Armor stuff + #LOC_BDArmory_ArmorWidth = Ширина + #LOC_BDArmory_ArmorWidthR = Ширина Справа + #LOC_BDArmory_ArmorWidthL = Ширина Слева + #LOC_BDArmory_ArmorLength = Длина + #LOC_BDArmory_ArmorAdjustParts = Изменять Прикрепленные Детали + #LOC_BDArmory_ArmorTriIso = Треугольник: Равнобедренный + #LOC_BDArmory_ArmorTriSca = Треугольник: Разносторонний + #LOC_BDArmory_Wood = Дерево + #LOC_BDArmory_Aluminium = Алюминий + #LOC_BDArmory_Steel = Сталь + #LOC_BDArmory_Armor_HullType = Материал Корпуса + #LOC_BDArmory_ArmorThickness = Толщина Брони + #LOC_BDArmory_EquivalentThickness = Эквивалентная Толщина Стали + #LOC_BDArmory_ArmorRemaining = Целостность Брони + #LOC_BDArmory_ArmorTotalMass = Общая Масса Брони + #LOC_BDArmory_ArmorTotalCost = Общая Стоимость Брони + #LOC_BDArmory_ArmorTotalLift = Общая Подъемная Сила + #LOC_BDArmory_ArmorWingLoading = Удельная Нагрузка Крыла + #LOC_BDArmory_ArmorStats = Свойства Брони + #LOC_BDArmory_ArmorStrength = Сила + #LOC_BDArmory_ArmorHardness = Твердость + #LOC_BDArmory_ArmorDuctility = Пластичность + #LOC_BDArmory_ArmorDiffusivity = Диффузия + #LOC_BDArmory_ArmorMaxTemp = Безопасн. Температура + #LOC_BDArmory_ArmorDensity = Плотность + #LOC_BDArmory_ArmorMass = Масса Брони + #LOC_BDArmory_ArmorCost = Стоимость + #LOC_BDArmory_ArmorCurrent = Текущая Броня + #LOC_BDArmory_ArmorVisualizer = Визуализация Брони + #LOC_BDArmory_ArmorHPVisualizer = Визуализация ХП + #LOC_BDArmory_ArmorHullVisualizer = Визуализация Корпуса + #LOC_BDArmory_ArmorLiftVisualizer = Визуализация Подъемной Силы + #LOC_BDArmory_ArmorSelect = Выбрать Материал Брони + #LOC_BDArmory_ArmorTool = BDA Инструмент Систем Аппарата + #LOC_BDArmory_Armor_HullMat = Текущий Материал Корпуса + #LOC_BDArmory_Armor_ArmorType = Тип Брони + #LOC_BDArmory_Armor_HullType = Тип Корпуса + #LOC_BDArmory_Armor_Hullmass = Масса Измененных Деталей + #LOC_BDArmory_BulletResist = Кин. Сопротивление + #LOC_BDArmory_ExplosionResist = Взрывоустойчивость + #LOC_BDArmory_LaserResist = Лазер. Сопротивление + #LOC_BDArmory_ArmorShatterWarning = Проникающий удар разрушит броню + #LOC_BDArmory_ArmorLiftStacking = Суммирование Подъемной Силы + + //Space hack stuff + #LOC_BDArmory_Settings_SpaceHacks = Инструменты Космических Боев + #LOC_BDArmory_Settings_SpaceFriction = Космическая Погрешность + #LOC_BDArmory_Settings_IgnoreGravity = Игнорировать Гравитацию + #LOC_BDArmory_Settings_Repulsor = Вкл. Эффект Отталкивания + #LOC_BDArmory_Settings_SpaceFrictionMult = Множитель Поворота + + //Mutator Gamemode stuff + #LOC_BDArmory_Settings_Mutators = Мутации + #LOC_BDArmory_MutatorSelect = Выбрать Мутации + #LOC_BDArmory_Settings_MutatorGlobal = Применить Глобально + #LOC_BDArmory_Settings_MutatorKill = Применить на Убийство + #LOC_BDArmory_Settings_MutatorTimed = Применить на Таймер + #LOC_BDArmory_Settings_MutatorDuration = Продолжительность + #LOC_BDArmory_UI_MutatorStart = Глобальные Мутации + #LOC_BDArmory_UI_MutatorShuffle = Мутации Перемешались! + #LOC_BDArmory_Settings_MutatorNum = Кол-во Мутаций + #LOC_BDArmory_Settings_MutatorIcons = Значки Мутаций + } +} diff --git a/BDArmory/Distribution/GameData/BDArmory/Localization/localization-de-de.cfg b/BDArmory/Distribution/GameData/BDArmory/Localization/localization-de-de.cfg new file mode 100644 index 000000000..cb9c99ed2 --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Localization/localization-de-de.cfg @@ -0,0 +1,416 @@ +Localization +{ + de-de + { + #loc_BDArmory_modname = BD Armory + + #loc_BDArmory_agent_title = Bahamuto Dynamics + #loc_BDArmory_agent_description = Bahamuto Dynamics ist ein führender Hersteller von Militärwaffen und -munition. Gelegentlich wird das Unternehmen vom Raumfahrtprogramm für fortschrittliche oder einzigartige technische Lösungen beauftragt. + #loc_BDArmory_part_manufacturer = Bahamuto Dynamics + + //Vulcan Turret + #loc_BDArmory_part_bahaGatlingGun_title = Vulkan-Geschützturm + //A 6 barrel 20x102mm rotary cannon. + #loc_BDArmory_part_bahaGatlingGun_description = Eine 6-läufige, drehbare 20x102mm Vulkan-Kanone. + + //.50cal Turret + #loc_BDArmory_part_bahaTurret_title = .50cal Maschinengewehrgeschützturm + //A dual barrel .50 cal machine gun. + #loc_BDArmory_part_bahaTurret_description = Ein zweiläufiger .50cal Maschinengewehrgeschützturm. + + //USAF Airborne Laser + #loc_BDArmory_part_bahaABL_title = USAF Laser + //A high powered laser for setting things on fire. Uses 350 electric charge per second. + #loc_BDArmory_part_bahaABL_description = Ein Hochleistungslaser, der Ziele in Brand setzt. Verbraucht 350 Elektrizitätseinheiten pro Sekunde. + + //Adjustable Missile Rail + #loc_BDArmory_part_bahaAdjustableRail_title = Justierbare Raketenschiene + //A rail for mounting missiles. + #loc_BDArmory_part_bahaAdjustableRail_description = Aufhängungspunkt für Raketen und Bomben. Abstand und Länge einstellbar. + + //AGM-86C Cruise Missile + #loc_BDArmory_part_bahaAgm86B_title = AGM-86C Marschflugkörper + //Long distance, sub-sonic, air-launched, GPS-guided cruise missile. This missile has no booster, so it must be launched while airborne at cruising speed. + #loc_BDArmory_part_bahaAgm86B_description = Luftgestarteter, GPS-geführter, Unterschallschneller Marschflugkörper. Diese Rakete hat keinen Booster und muss deshalb bei Reisefluggeschwindigkeit aus der Luft gestartet werden. + + //AIM-120 AMRAAM Missile + #loc_BDArmory_part_bahaAim120_title = AIM-120 AMRAAM Luft-Luft Rakete + //Medium range radar guided homing missile. + #loc_BDArmory_part_bahaAim120_description = Radargeführte Luft-Luft Rakete mittlerer Reichweite. + + //AIM-120 AMRAAM EMP Missile + #loc_BDArmory_part_bahaEMP120_title = AIM-120 AMRAAM Luft-Luft Rakete (EMP) + //Medium range radar guided homing missile. + #loc_BDArmory_part_bahaEMP120_description = Radargeführte Luft-Luft Rakete mittlerer Reichweite mit elektromagnetischem Impuls-Wirksystem. Die Rakete verursacht minimalen strukturellen Schaden, aber und macht alle elektronischen Geräte innerhalb von 100 Metern Radius funktionsunfähig. + + //AI Pilot Flight Computer + #loc_BDArmory_part_bdPilotAI_title = KI Autopilot (für Flugzeuge) + //Flies your plane on combat air patrol missions without using your hands. Tune the values based on your plane's unique flight characteristics. Please activate engines manually. Works in conjunction with a weapon manager in guard mode (attach and configure separately). (EXPERIMENTAL) + #loc_BDArmory_part_bdPilotAI_description = Autopilot, der ein Flugzeug auf Luftpatruille steuern kann. Einige Parameter müssen den Eigenschaften des Flugzeugs entsprechend justiert werden. Kann mit Waffensteuerungsgerät im Wächtermodus (separate Komponente) kombiniert werden. Motor(en) manuell starten! (EXPERIMENTELL) + + //AI Surface Operation Driver + #loc_BDArmory_part_bdDriverAI_title = KI Autopilot (Schiffe und Fahrzeuge) + //Drives your car/tank/boat/etc on combat and patrol missions over the lands and seas without using your hands. Tune the values based on your ship's unique characteristics. Please activate engines manually. Works in conjunction with a weapon manager in guard mode (attach and configure separately). (EXPERIMENTAL) + #loc_BDArmory_part_bdDriverAI_description = Autopilot, der ein Fahrzeug (Auto, Panzer, Boot, etc.) auf Patruille steuern kann. Einige Parameter müssen den Eigenschaften des Flugzeugs entsprechend justiert werden. Kann mit Waffensteuerungsgerät im Wächtermodus (separate Komponente) kombiniert werden. Motor(en) manuell starten! (EXPERIMENTELL) + + //20mm Ammunition Box + #loc_BDArmory_part_baha20mmAmmo_title = 20mm Munitionskasten + //Ammo box containing 650 20x102mm rounds. + #loc_BDArmory_part_baha20mmAmmo_description = Munitionskasten für 650 20x102mm Patronen. + + //25mm Ammunition Box + #loc_BDArmory_part_baha25mmAmmo_title = 25mm Munitionskasten + //Ammo box containing 650 20x102mm rounds. + #loc_BDArmory_part_baha25mmAmmo_description = Munitionskasten für 625 25x137mm Patronen. + + //30mm Ammunition Box + #loc_BDArmory_part_baha30mmAmmo_title = 30mm Munitionskasten + //Ammo box containing 600 30x173mm rounds. + #loc_BDArmory_part_baha30mmAmmo_description = Munitionskasten für 600 30x173mm Patronen. + + //70mm Rocket Ammunition Box + #loc_BDArmory_part_rocket70mmAmmo_title = 70mm Paketenmunitionskasten + //Ammo box containing 48 70mm Rockets. + #loc_BDArmory_part_rocket70mmAmmo_description = Raketenmunitionskasten für 48 ungesteuerte 70mm Paketen. + + //50cal Ammunition Box + #loc_BDArmory_part_baha50CalAmmo_title = 50cal Munitionskasten + //Ammo box containing 1200 .50 cal rounds. + #loc_BDArmory_part_baha50CalAmmo_description = Munitionskasten für 1200 .50 cal Patronen. + + //Universal Ammo Box (Legacy) + #loc_BDArmory_part_UniversalAmmoBoxBDA_title = Universal-Munitionskasten (Altbestand) + //(Obsolete - DO NOT USE - Requires Fire Spitter) Scalable Ammo box containing whatever ammo you want to put in it, does hold a selectable quantity of every ammunition type up to 16'1 inch that is currently used in KSP in association with BDAc Extra types can be added upon request (no fantasy ammo please) NOTE: this part still requires Fire Spitter, and is here for backwards compatability. Use the new UniversalAmmo part going forward. + #loc_BDArmory_part_UniversalAmmoBoxBDA_description = (Veraltet - NICHT BENUTZEN - Benötigt Fire Spitter Mod) Skalierbare Munitionskiste für jede Munition in jeder beliebigen Menge. Mischen aller Munitionstypen bis 16'1 Zoll möglich. Funktioniert in KSP mit BDAc Mod. Weitere Munitionstypen können auf Anfrage unterstützt werden (bitte keine Phantasie-Munition!). HINWEIS: Dieses Bauteil benötigt die Fire Spitter Mod und steht nur zum Zweck der Rückwärtskompatibilität zu Verfügung. Bitte ab sofort die nur den neuen Universal-Munitionskasten verwenden. + + //Universal Ammo Box + #loc_BDArmory_part_BDAcUniversalAmmoBox_title = Universal Ammo Box + //Scaleable Ammo box containing whatever ammo you want to put in it, does hold a selectable quantity of every ammunition type up to 16'1 inch that is currently used in KSP in association with BDAc Extra types can be added upon request (no fantasy ammo please) + #loc_BDArmory_part_BDAcUniversalAmmoBox_description = Skalierbare Munitionskiste für jede Munition in jeder beliebigen Menge. Mischen aller Munitionstypen bis 16'1 Zoll möglich. Funktioniert in KSP mit BDAc Mod. Weitere Munitionstypen können auf Anfrage unterstützt werden (bitte keine Phantasie-Munition!). + + //Cannon Ammunition Box + #loc_BDArmory_part_bahaCannonShellBox_title = Kanonenkugel-Munitionskiste + //Ammo box containing 10 cannon shells. + #loc_BDArmory_part_bahaCannonShellBox_description = Munitionskiste für 10 Kanonenkugeln. + + //BD 1x1 slope Armor + #loc_BDArmory_part_BD1x1slopeArmor_title = BD 1x1 dreieckige Panzerplatte + //A sturdy 1x1 slope Armor plate, perfect for constructing all sorts of things. PS does not float + #loc_BDArmory_part_BD1x1slopeArmor_description = Eine stabile, dreieckige 1x1 Panzerplatte, perfekt zum Konstruieren aller möglichen Dinge. PS: schwebt nicht! + + //BD 2x1 slope Armor + #loc_BDArmory_part_BD2x1slopeArmor_title = BD 2x1 dreieckige Panzerplatte + //A sturdy 2x1 slope Armor plate, perfect for constructing all sorts of things. PS does not float + #loc_BDArmory_part_BD2x1slopeArmor_description = Eine stabile, dreieckige 2x1 Panzerplatte, perfekt zum Konstruieren aller möglichen Dinge. PS: schwebt nicht! + + //BD 1x1 panel Armor + #loc_BDArmory_part_BD1x1panelArmor_title = BD 1x1 Panzerplatte + //A sturdy 1x1 Armor plate, perfect for constructing all sorts of things. PS does not float + #loc_BDArmory_part_BD1x1panelArmor_description = Eine stabile 1x1 Panzerplatte, perfekt zum Konstruieren aller möglichen Dinge. PS: schwebt nicht! + + //BD 2x1 panel Armor + #loc_BDArmory_part_BD2x1panelArmor_title = BD 2x1 Panzerplatte + //A sturdy 2x1 Armor plate, perfect for constructing all sorts of things. PS does not float + #loc_BDArmory_part_BD2x1panelArmor_description = Eine stabile 2x1 Panzerplatte, perfekt zum Konstruieren aller möglichen Dinge. PS: schwebt nicht! + + //BD 3x1 panel Armor + #loc_BDArmory_part_BD3x1panelArmor_title = BD 3x1 Panzerplatte + //A sturdy 3x1 Armor plate, perfect for constructing all sorts of things. PS does not float + #loc_BDArmory_part_BD3x1panelArmor_description = Eine stabile 3x1 Panzerplatte, perfekt zum Konstruieren aller möglichen Dinge. PS: schwebt nicht! + + //BD 4x1 panel Armor + #loc_BDArmory_part_BD4x1panelArmor_title = BD 4x1 Panzerplatte + //A sturdy 4x1 Armor plate, perfect for constructing all sorts of things. PS does not float + #loc_BDArmory_part_BD4x1panelArmor_description = Eine stabile 4x1 Panzerplatte, perfekt zum Konstruieren aller möglichen Dinge. PS: schwebt nicht! + + //AWACS Detection Radar + #loc_BDArmory_part_awacsRadar_title = AWACS luftgestütztes Radarsytem + //A large radar capable of detecting objects from a long distance. This radar does NOT have the capability of tracking or locking targets. + #loc_BDArmory_part_awacsRadar_description = Großes Radarsystem zur Luftraumüberwachung. Erkennt Flugzeuge aus großer Distanz. Dieses Radar ist NICHT in der Lage, Ziele zu verfolgen oder zu erfassen. + + //Modular Missile Guidance (EXPERIMENTAL) + #loc_BDArmory_part_bdammGuidanceModule_title = Modulares Raketenführungssystem (EXPERIMENTAL) + //A missile guidance computer. Manually tune steering settings to craft's unique flight characteristics. Select a guidance mode. Select a target then enable guidance. Activate engines and stages manually. (EXPERIMENTAL) + #loc_BDArmory_part_bdammGuidanceModule_description = Raketenführungssystem. Einige Parameter müssen den Eigenschaften der Rakete entsprechend justiert werden. Wähle einen Führungsmodus. Wähle ein Ziel. Aktiviere die Zielnachführung. Aktiviere Antrieb und Raketenstufen manuell. (EXPERIMENTAL) + + //Browning .50cal AN/M2 + #loc_BDArmory_part_bahaBrowningAnm2_title = Browning .50cal AN/M3 + //An old fixed .50 cal machine gun 50cal ammo + #loc_BDArmory_part_bahaBrowningAnm2_description = Eine altes .50 cal Maschiengewehr für 50cal Minution. + + //CBU-87 Cluster Bomb + #loc_BDArmory_part_bahaClusterBomb_title = CBU-87 Streubombe + //This bomb splits open and deploys many small bomblets at a certain altitude. + #loc_BDArmory_part_bahaClusterBomb_description = Diese Bombe öffnet sich nach kurzer Fallzeit, in einer einstellbaren Höhe, und gibt Kleinbomben frei. + + //Chaff Dispenser + #loc_BDArmory_part_bahaChaffPod_title = Düppelabwurfvorrichtung + //Drops chaff for confusing or breaking radar locks. + #loc_BDArmory_part_bahaChaffPod_description = Wirft Wolken von Düppeln ab, die die Radarverfolgung stören oder brechen können. + + //Flare Dispenser + #loc_BDArmory_part_bahaCmPod_title = IR-Täuschkörper-Abwurfvorichtung + //Drops flares for confusing heat-seeking missiles. + #loc_BDArmory_part_bahaCmPod_description = Wirft Täuschkörper ab, die die Zielverfolgung von hitzesuchenden Raketen verwirren können. + + //AN/ALQ-131 ECM Jammer + #loc_BDArmory_part_bahaECMJammer_title = Elektronische Gegenmaßnahmen AN/ALQ-131 + //This electronic device makes it harder for radars to lock onto your vehicle, and increases your chances of breaking the lock. + #loc_BDArmory_part_bahaECMJammer_description = Dieses System erschwert das Aufschalten des Flugzeugs durch feindliches Radar. Kann die Radaraufschaltung brechen. + + //GAU-8 30x173mm Cannon + #loc_BDArmory_part_bahaGau-8_title = GAU-8 Gatling-Kanone + //A 7 barrel 30mm rotary cannon. + #loc_BDArmory_part_bahaGau-8_description = Siebenläufige Gatling-Kanone für 30x173mm Patronen. + + //Goalkeeper CIWS + #loc_BDArmory_part_bahaGoalKeeper_title = Goalkeeper Nahbereichsverteidigungssystem + //A 7 barrel 30mm rotary cannon with full swivel range. The 30mm high explosive rounds self detonate at the set distance, but this weapon does not feature automatic fuse timing. It has its own detection & tracking radar, though that is only effective at close range and does not replace a proper volumen serach radar + #loc_BDArmory_part_bahaGoalKeeper_description = Siebenläufige Gatling-Kanone für 30x173mm Patronen mit vollem Bewegungsbereich von 360 Grad rundum, -15 bis 85 Grad Schusswinkel. Die 30mm Hochexplosivgeschosse detonieren in der Nähe des Ziels, jedoch ohne adaptive Zündverzögerung. Das System hat ein eigenes Radar für die Aufschaltung und Verfolgung von Zielen. Nur in Nahbereich effektiv - ersetzt kein Weitbereichsradar! + + //GoalkeeperMk1 CIWS + #loc_BDArmory_part_GoalKeeperBDAcMk1_title = GoalkeeperMk1 Nahbereichsverteidigungssystem + //A 7 barrel 30mm rotary cannon with full swivel range.This MK 1 version was found under a tarpaulin in a muddy field, Perfect for cash strapped militias and shifty governments (cheapskate version) Without Radar or detection equipment this turret requires the target information to be fed from an alternative source.(somebody pointing and shouting 'shoot that' has been found to be only marginally effective due to the excessive noise produced when the weapon fires) The 30mm high explosive rounds self detonate when they lose interest in flying, but this weapon does not feature automatic fuse timing. + #loc_BDArmory_part_GoalKeeperBDAcMk1_description = Siebenläufige Gatling-Kanone für 30x173mm Patronen mit vollem Bewegungsbereich von 360 Grad rundum, -15 bis 85 Grad Schusswinkel. Diese Mk1-Version wurde in einem schlammigen Feld unter einer Plane gefunden - perfekt für bargeldlose Milizen und zwielichtige Regierungen. Ohne Radar oder Detektionssystem. Zielinformationen müssen anderweitig zugeführt werden. (Zeigen und 'Schieß das ab!'-Rufen hat sich aufgrund der lauten Schussgeräusche als annähernd ineffektiv erwiesen.) Die 30mm Hochexplosivgeschosse detonieren irgendwann, wenn sie nicht mehr fliegen wollen, jedoch ohne adaptive Zündverzögerung. + + //Goalkeeper MK2 CIWS + #loc_BDArmory_part_BDAcGKmk2_title = Goalkeeper MK2 Nahbereichsverteidigungssystem + //A 7 barrel 30mm rotary cannon with full swivel range. This MK 2 version was found covered in overspray and paint cans around the back of the hangar at the old KSC, developed from the MK1 to reduce the incidence of hearing loss amongst early target pointers. This MK2 has some slight advantages over the MK1, equipped with Infra red targeting and Radar data receiver The 30x173mm high explosive rounds are only a slight improvement over the MK1 ammunition in that they at least take slightly longer to lose interest in flying and so have a good chance of reaching the target, but this weapon was never equipped to feature automatic fuse timing. + #loc_BDArmory_part_BDAcGKmk2_description = Siebenläufige Gatling-Kanone für 30x173mm Patronen mit vollem Bewegungsbereich von 360 Grad rundum, -15 bis 85 Grad Schusswinkel. Diese Mk2-Version wurde im alten KSC im hinteren Teil des Hangars mit Lacknebel und Sprühdosen bedeckt aufgefunden. Es wurde aus dem MK1 entwickelt, um die Häufigkeit von Gehörverlusten bei Zielsuchern zu verringern. Mk2 hat gegenüber Mk1 leichte Vorteile, da es Infrarot-Zielaufschaltung und einen Radardatenempfänger hat. Die 30mm Hochexplosivgeschosse detonieren etwas später als die der Mk1-Version, jedoch gibt es auch hier keine adaptive Zündverzögerung. + + //TWS Locking Radar + #loc_BDArmory_part_scanLockRadar1_title = Track-while-scan Radarverfolgungssystem + // This unit has a medium range detection radar and a built-in target tracking radar. This radar is capable of locking targets, and will continue to scan while tracking the locked target (TWS - Track While Scan). It is optimized for air search&track, and has difficulties detecting and tracking surface targets. + #loc_BDArmory_part_scanLockRadar1_description = Dieses Radarsystem hat eine Detektionssystem mittlere Reichweite und ein zusätzliches Zielverfolgungsradar. Es kann Ziele aufschalten und verfolgen und dabei weiter nach Zielen scannen. Das System ist optimiert für die Luftraumüberwachung und schwächelt bei der Detektion und Verfolgung von Bodenzielen. + + //Large Detection Radar + #loc_BDArmory_part_scanLargeRadar_title = Großes Suchradar + // A large radar capable of detecting objects from a long distance. This radar does NOT have the capability of tracking or locking targets. It is optimized for air search, and has difficulties detecting surface targets. + #loc_BDArmory_part_scanLargeRadar_description = Ein großes Suchradar, das Objekte in großer Entfernung detektieren kann. Dieses Radar hat keine Funktion zur Zielaufschaltung oder -verfolgung! Es ist optimiert für die Luftraumüberwachung und schwächelt bei der Detektion von Bodenzielen. + + //F-86 Launcher + #loc_BDArmory_part_F86RL_title = FFAR Abschussystem für ungelenkte Raketen + // Internally-mounted Rocket Launcher designed for Air-to-Air use. Holds 24 unguided Folding-Fin Aerial Rockets. Can be reloaded from an ammo bin when empty. + #loc_BDArmory_part_F86RL_description = Ein Abschusssystem für ungelenkte Raketen, das in den Flugzeugrumpf einbegaut wird. Enthält 24 Raketen mit ausklappbaren Stabilisatoren (Folding Fin Aerial Rockets). Kann aus einer Munitionskiste nachgeladen werden. + + //Hydra-70 Rocket Pod + #loc_BDArmory_part_bahaH70Launcher_title = Abschusssystem-Gondel für ungelenkte Hydra-70 Raketen + //Holds and fires 19 unguided Hydra-70 rockets. + #loc_BDArmory_part_bahaH70Launcher_description = Gondel für Montage unter den Tragflächen. Enthält 19 ungelenkte Hydra-70 Raketen. + + //Hydra-70 Rocket Turret + #loc_BDArmory_part_bahaH70Turret_title = Bewegliches Abschusssystem für ungelenkte Hydra-70 Raketen + //Turret pod that holds and fires 32 unguided Hydra-70 rockets. + #loc_BDArmory_part_bahaH70Turret_description = Abschusssystem für ungelekte Raketen mit vollem Bewegungsbereich von 360 Grad rundum, -30 bis 35 Grad Schusswinkel. Enthält 32 Hydra-70 Raketen. + + //AGM-88 HARM Missile + #loc_BDArmory_part_bahaHarm_title = AGM-88 HARM Rakete + //High-speed anti-radiation missile. This missile will home in on radar sources detected by the Radar Warning Receiver. + #loc_BDArmory_part_bahaHarm_description = Hochgeschwindigkeits-Anti-Radar-Rakete. Diese Rakete lenkt auf Radarziele, die von einem Radar-Warn-Empfänger entdeckt werden. + + //HE-KV-1 Missile + #loc_BDArmory_part_bahaHEKV1_title = HE-KV-1 Rakete + //The HE-KV-1 (High explosive kill vehicle) is a radar-guided homing missile that uses reaction control thrusters and thrust vectoring to maneuver. This means it is capable of steering towards targets in a vacuum. + #loc_BDArmory_part_bahaHEKV1_description = Die HE-KV-1 (High explosive kill vehicle, Hochexplosiv-Tötungsvehikel) ist eine Radargelenkte Rakete, die mit Reaktionssteuerungsdüsen und Schubvektorkontrolle manövriert und dadurch auch Ziele im Vakuum ansteuern kann. + + //AGM-114 Hellfire Missile + #loc_BDArmory_part_bahaAGM-114_title = AGM-114 Hellfire Rakete + //Small, quick, laser guided homing missile. + #loc_BDArmory_part_bahaAGM-114_description = Kleine, schnelle, Laser-geführte Lenkrakete. + + //AGM-114 Hellfire II EMP Missile + #loc_BDArmory_part_bahaAGM-114_EMP_title = AGM-114R Hellfire II EMP Rakete + //Small, quick, laser guided homing missile equipped with the latest miniaturized EMP warhead. While the pulse radius is small (50 meters), it is quite effective. The missile does minimal structural damage, but renders all electronic devices within it's blast radius inoperable. + #loc_BDArmory_part_bahaAGM-114_EMP_description = Kleine, schnelle, Laser-geführte Lenkrakete, die mit neuester miniaturisierter EMP Technologie ausgestattet ist. Das elektromagnetische Impuls-System hat einen Wirkradius von nur 50 Metern, ist aber höchst effektiv. Die Rakete verursacht minimalen strukturellen Schaden, aber und macht alle elektronischen Geräte innerhalb des Wirkradius funktionsunfähig. + + //Vulcan (Hidden) + #loc_BDArmory_part_bahaHiddenVulcan_title = Vulkan-Kanone (versteckt) + //A 6 barrel 20x102mm rotary cannon. 20x102Ammo + #loc_BDArmory_part_bahaHiddenVulcan_description = Eine 6-läufige 20x102mm Vulkan-Kanone. + + //Mk83 JDAM Bomb + #loc_BDArmory_part_bahaJdamMk83_title = Mk83 JDAM Bombe + //1000lb GPS-guided bomb. + #loc_BDArmory_part_bahaJdamMk83_description = 1000 Pfund GPS-geführte Bombe. + + //M102 Howitzer (Radial) + #loc_BDArmory_part_bahaM102Howitzer_title = M102 Howitzer-Kanone (Radial) + //A radially mounted 105mm gun. CannonShells + #loc_BDArmory_part_bahaM102Howitzer_description = Eine radial montierte 105mm Kanone. Feuert Kanonenkugeln. + + //M1 Abrams Cannon + #loc_BDArmory_part_bahaM1Abrams_title = M1 Abrams Kanone + //A 120mm cannon on an armored turret. CannonShells + #loc_BDArmory_part_bahaM1Abrams_description = Eine 120mm Kanone auf einem gepanzerten Geschützturm. Feuert Kanonenkugeln. + + //M230 Chain Gun Turret + #loc_BDArmory_part_bahaM230ChainGun_title = M230 Maschinengewehr-Geschützturm + //The M230 Chain Gun is a single-barrel automatic cannon firing 30x173 Ammo high explosive rounds. It is commonly used on attack helicopters. + #loc_BDArmory_part_bahaM230ChainGun_description = Des M230 Maschinengewehr-Geschützturm ist ein einläufiges Maschinengewehr mit einem Drehbereich von 270 Grad horizontal und Schusswinkel -17 bis 50 Grad vertikal. Feuert 30x173 mm Hochexplosivgeschosse. Üblicherweise auf Kampfhubschraubern eingesetzt. + + //AGM-65 Maverick Missile + #loc_BDArmory_part_bahaAGM-65_title = AGM-65 Maverick Rakete + //Medium yield laser guided air-to-ground missile. + #loc_BDArmory_part_bahaAGM-65_description = Lasergeführte Luft-Boden-Rakete mittlerer Durchschlagskraft. + + //Jernas Missile Turret + #loc_BDArmory_part_missileTurretTest_title = Jernas Raketen-Geschützturm + //A turret capable of holding and firing up to 8 small to medium sized missiles. Comes with an integrated detection and tracking radar. Warranty void if anything except missiles are mounted. To enable the turret, select the mounted missile from the weapon manager. + #loc_BDArmory_part_missileTurretTest_description = Ein Geschützturm für 8 kleine bis mittelgroße Raketen. Drehbereich 360 Grad horizontal und Schusswinkel -8 bis 75 Grad vertikal. Eingebautes Zielerkennungs und -verfolgungs-Radar. Die Garantie erlischt, wenn irgendetwas anderes als Raketen montiert werden. Um den Geschützturm zu aktivieren, müssen die geladenen Raketen im Waffen-Kontrollsystem ausgewählt werden. + + //Mk82 Bomb + #loc_BDArmory_part_bahaMk82Bomb_title = Mk82 Bombe + //500lb unguided bomb. + #loc_BDArmory_part_bahaMk82Bomb_description = Ungelenkte 500-Pfund-Bumbe. + + //Mk82 SnakeEye Bomb + #loc_BDArmory_part_bahaMk82BombBrake_title = Mk82 Bombe 'SnakeEye' + //500lb unguided bomb with airbrakes. Use for low altitude bombing. + #loc_BDArmory_part_bahaMk82BombBrake_description = Ungelenkte 500-Pfund-Bombe mit Luftbremse. Für Bombardierung aus geringer Höhe verwenden. + + //Oerlikon Millennium Cannon + #loc_BDArmory_part_bahaOMillennium_title = Oerlikon Millennium Revolverkanone + //A turret that fires timed detonation explosive rounds. Suited for close-in air defense. A device at the muzzle end of the barrel measures the exact speed of each round as it is fired, and automatically sets the fuse to detonate the round as it approaches a pre-set distance from the target. Uses 30x173Ammo + #loc_BDArmory_part_bahaOMillennium_description = Ein Geschützturm mit Revolver-Maschinenkanone für Explosivgeschosse mit adaptiver Zündverzögerung. Geeignet für Nahbereichs-Luftverteidigung. Ein Anlage zur Messung der Mündungsgeschwindigkeit bestimmt die Geschwindigkeit jeder gefeuerten Kugel und stellt die Zündverzögerung automatisch so ein, dass die Munition in einem vor-eingestellten Abstand zum Ziel detoniert. Für 30x173 30mm Munition. + + //PAC-3 Intercept Missile + #loc_BDArmory_part_bahaPac-3_title = PAC-3 Abfangrakete + //Medium range, high speed, radar-guided surface to air missile. + #loc_BDArmory_part_bahaPac-3_description = Radargelenkte Hochgeschwindigkeits-Boden-Luft-Rakete mittlerer Reichweite. + + //Patriot Launcher Turret + #loc_BDArmory_part_patriotLauncherTurret_title = Patriot Raketengeschützturm + //A turret capable of holding and firing up to 16 PAC-3 missiles (4 per cannister). Warranty void if anything except missiles are mounted. To enable the turret, select the mounted missile from the weapon manager. + #loc_BDArmory_part_patriotLauncherTurret_description = Eine dreh und schwenkbare Abschussvorrichtung für bis zu 16 PAC-3 Raketen (4 pro Kanister). Die Garantie erlischt, wenn irgendetwas anderes als Raketen geladen wird. Um den Geschützturm zu aktivieren, müssen die geladenen Raketen im Waffen-Kontrollsystem ausgewählt werden. + + //Radar Data Receiver + #loc_BDArmory_part_radarDataReceiver_title = Radar-Datenempfänger + //A module that can display radar contacts via data-link and lock targets through a remote radar, but can not scan or lock by itself. Useful for a hidden missile battery. + #loc_BDArmory_part_radarData_description = Ein Modul, das Radarkontakte eines via Datenverbindung verbundenen Radarsystems anzeigen und verfolgen kann. Enthält kein Radarsystem! Nützlich für versteckte Raketen-Batterien. + + //AN/APG-63 Radome + #loc_BDArmory_part_bdRadome1_title = AN/APG-63 Radom + // A forward facing, aerodynamically housed radar. It can scan and lock targets within a 120 degree field of view. It is optimized for air-to-air combat, and has difficulties locking surface targets. + #loc_BDArmory_part_bdRadome1_description = Vorwärts gerichtetes, aerodynamisch verpacktes Bordradar zur Detektion und Verfolgung von Zielen innerhalb eines Sichtfeldes von 120 Grad. Optimiert für die Luftraumüberwachung, schwächelt bei der Detektion und Verfolgung von Bodenzielen. + + //AN/APG-63 Inline Radome + #loc_BDArmory_part_bdRadome1inline_title = AN/APG-63 Inline Radom + // A forward facing, aerodynamically housed radar. It can scan and lock targets within a 120 degree field of view. Make sure the black markings are pointing forward. It is optimized for air-to-air combat, and has difficulties locking surface targets. + #loc_BDArmory_part_bdRadome1inline_description = Vorwärts gerichtetes, aerodynamisch verpacktes Bordradar zur Detektion und Verfolgung von Zielen innerhalb eines Sichtfeldes von 120 Grad. Optimiert für die Luftraumüberwachung, schwächelt bei der Detektion und Verfolgung von Bodenzielen. + + //AN/APG-63 Radome + #loc_BDArmory_part_bdRadome1snub_title = AN/APG-63 Radom + // A forward facing, aerodynamically housed radar. It can scan and lock targets within a 120 degree field of view. This is a dedicated ground attack version with much better performance against ground targets, but reduced air-to-air capabilities. + #loc_BDArmory_part_bdRadome1inline_description = Vorwärtsgerichtetes, aerodynamisch verpacktes Bordradar zur Detektion und Verfolgung von Zielen innerhalb eines Sichtfeldes von 120 Grad. Optimiert für Bodenziele. Schwächelt bei der Detektion und Verfolgung von Luftzielen. + + //RBS-15 Cruise Missile + #loc_BDArmory_part_bahaRBS-15Cruise_title = RBS-15 Boden-Boden Marschflugkörper + //Long distance, multi-platform high-speed cruise missile with boosters. + #loc_BDArmory_part_bahaRBS-15Cruise_description = Marschflugkörper hoher Reichweite und hoher Geschwindigkeit mit Raketenboostern. + + //RBS-15 Air launched Cruise Missile + #loc_BDArmory_part_bahaRBS-15ALCruise_title = RBS-15 Luft-Boden Marschflugkörper + //Long distance, multi-platform high-speed cruise missile with boosters. + #loc_BDArmory_part_bahaRBS-15ALCruise_description = Marschflugkörper hoher Reichweite und hoher Geschwindigkeit mit Raketenboostern. Variante für Abwurf von einem Flugzeug. + + //Adjustable Rotary Bomb Rack + #loc_BDArmory_part_bdRotBombBay_title = Justierbares, rotierendes Bombenabwurfsystem + //An adjustable rotary bomb rack. The yellow arrow should be pointing in the direction of weapon release. Missiles or bombs only. One per rail only. + #loc_BDArmory_part_bdRotBombBay_description = Justierbares, rotierendes Abwurfsystem für 8 Raketen oder Bomben. Der gelbe Pfeil sollte in die Abwurfrichtung zeigen. Nur für Raketen und Bomben (eine Pro Schiene). + + //S-8KOM Rocket Pod + #loc_BDArmory_part_bahaS-8Launcher_title = Abschusssystem-Gondel für ungelenkte S-8KOM Raketen + //Holds and fires 23 unguided S-8KOM rockets. It has an aerodynamic nose cone. + #loc_BDArmory_part_bahaS-8Launcher_description = Gondel für Montage unter den Tragflächen. Enthält 23 ungelenkte S-8KOM Raketen. Aerodynamischer Bugkonus. + + //AIM-9 Sidewinder Missile + #loc_BDArmory_part_bahaAim9_title = AIM-9 Sidewinder Rakete + //Short range heat seeking missile. + #loc_BDArmory_part_bahaAim9_description = Hitzesuchende Rakete kurzer Reichweite. + + //Small High Explosive Warhead + #loc_BDArmory_part_bdWarheadSmall_title = Kleiner hochexplosiver Gefechtskopf + //A missile nose cone packed with explosives. + #loc_BDArmory_part_bdWarheadSmall_description = Ein mit Sprengstoff vollgestopfter Raketengefechtskopf. + + //Smoke Countermeasure Pod + #loc_BDArmory_part_bahaSmokeCmPod_title = Abschussvorrichtung für Rauch-Gegenmaßnahmen + //Fires smoke-screen countermeasures for occluding laser points. + #loc_BDArmory_part_bahaSmokeCmPod_description = Feuert Gegenmaßnahmen, die eine Nebelwand erzeugen. Effektiv gegen Laser-Zielsysteme. + + //FLIR Targeting Ball + #loc_BDArmory_part_bahaFlirBall_title = FLIR Zielerfassungssystem + //A ball camera used for targeting and surveillance. Equipped with a high resolution camera with surface and horizon stabilization, and an infrared laser for painting targets, this pod allows you to quickly find and lock grounded targets for missiles. + #loc_BDArmory_part_bahaFlirBall_description = Horizont-stabilisiertes Infrarot-Kamerasystem mit Zielbeleuchtungs-Laser für die schnelle Detektion und Aufschaltung von Bodenzielen. + + //AN/AAQ-28 Targeting Pod + #loc_BDArmory_part_bahaCamPod_title = AN/AAQ-28 Zielerfassungssystem + //A targeting pod used for targeting and surveillance. Equipped with a high resolution camera with surface and horizon stabilization, and an infrared laser for painting targets, this pod allows you to quickly find and lock grounded targets for missiles. + #loc_BDArmory_part_bahaCamPod_description = Horizont-stabilisiertes Infrarot-Kamerasystem mit Zielbeleuchtungs-Laser für die schnelle Detektion und Aufschaltung von Bodenzielen. + + //Tow Launcher + #loc_BDArmory_part_towLauncherTurret_title = TOW Abschusssystem + //A turret capable of holding and firing up to 4 TOW missiles. Warranty void if anything except TOW missiles are mounted. To enable the turret, select the mounted missile from the weapon manager. + #loc_BDArmory_part_towLauncherTurret_description = Abschusssystem für 4 lasergeführte TWO Anti-Panzer-Raketen. Schwenkbereich 160 Grad, Schusswinkel -8 bis 12 Grad. + + //BGM-71 Tow Missile + #loc_BDArmory_part_bahaTowMissile_title = BGM-71 Tow Missile + //Short distance, laser beam-riding, wireless anti-tank missile. + #loc_BDArmory_part_bahaTowMissile_description = Lasergelenkte Kurzdistanz-Anti-Panzer-Rakete. + + //Weapon Manager + #loc_BDArmory_part_missileController_title = Waffen-Kontrollsystem + //Cycle through missiles/bombs and fire them with a single button. + #loc_BDArmory_part_missileController_description = Listet alle Waffensysteme. Wähle eins aus und feure es ab - mit einem einzigen Klick. + + //BDAsonarPod1A + #loc_BDArmory_part_BDAsonarPod1A_title = BDA MK1 Sonar + //BDA MK1 Sonar Pod can only detect splashed and submerged vessels mount below waterline for best results. As a hull-mounted sonar it has limited range and sensitivity only. + #loc_BDArmory_part_BDAsonarPod1A_description = Das BDA Mk1 Sonar detektiert nur schwimmende und untergetauchte Ziele. Auf de Außenhülle moniert. Geringe Reichweite und Empfindlichkeit. + + //StingRayBDATorpedo + #loc_BDArmory_part_StingRayBDATorpedo_title = Sting Ray BDA LightWeight Torpedo + #loc_BDArmory_part_StingRayBDATorpedo_description = Sting Ray Light Weight Torpedo Ship launch, and heli launch airdrop do not use in submarines. Interesting fact, you can fit 16 of these in a pac launcher, though using them in such a device without proper training has been the cause of much weeping and letters written. + + //SaturnAL31 + #loc_BDArmory_part_SaturnAL31_title = Saturn AL-31FM1 Strahltriebwerk mit Nachbrenner + //A high performance jet engine with a variable geometry thrust vectoring nozzle and an afterburner for extra thrust. Based on the highly popular J-404 engine, KTech engineers saw the potential of (highly) modifying the commercial variant into a formidable powerplant for military use. After seeing the potential of the engine, the BDAc group immediately licensed it for their new MkIII test drone. + #loc_BDArmory_part_SaturnAL31_description = Hochleistungsstrahltriebwerk mit Schubvektor-Düse mit variabler Geometrie und Nachbrenner für zusätzlichen Schub. Auf der Grundlage des beliebten J-404-Triebwerks sahen die KTech-Ingenieure das Potenzial, die kommerzielle Variante in ein beeindruckendes Triebwerk für militärische Zwecke zu verwandeln. Nachdem sie das Potenzial des Triebwerks erkannt hatten, lizenzierte die BDAc-Gruppe es sofort für ihre neue MkIII-Testdrohne. + + //GravityGun + #loc_BDArmory_part_GravGun_title = Nullpunkt-Energiefeld-Manipulator + // A very high-tech gun that weaponizes gravity. Seemingly powered by a mix of Technosorcery and Alien technology, this weapon is capable of applying non-Newtonian forces to targets, as well as affecting their apparent mass. Bahamuto Dynamics is not liable for any damages (to yourself, property, or the fabric of reality) incurred by this weapon. + #loc_BDArmory_part_GravGun_description = Eine Hightech-Waffe, die die Schwerkraft als Wirksystem einsetzt. Anscheinend durch eine Mischung aus Technosorcery und Alien-Technologie angetrieben, ist diese Waffe in der Lage, nicht-newtonsche Kräfte auf Ziele anzuwenden und ihre scheinbare Masse zu beeinflussen. Bahamuto Dynamics haftet nicht für Schäden, die durch diese Waffe entstehen (an Ihnen selbst, an Ihrem Eigentum oder an der Struktur der Realität). + + //Genie + #loc_BDArmory_part_genie_title = AIR-2 Genie Air-To-Air Rocket + //1.5kt Nuclear Anti-Air Rocket. + #loc_BDArmory_part_genie_description = 1.5 Kilotonnen Luft-Luft-Nuklearwaffe. + + //GAU-22 + #loc_BDArmory_part_GAU22_title = GAU-22/A 25x137mm Kanone + #loc_BDArmory_part_GAU22_description = Ein vierläufiges Maschinengewehr für 25x137mm Munition. + + //Sidam + #loc_BDArmory_part_sidam_title = Sidam Anti-Air gun + // A salvo-firing quad 25mm anti-air gun. 25x137mmAmmo. + #loc_BDArmory_part_sidam_description = Ein 4-fach anti-Luft Maschinengewehr-Abwehrgeschütz für 25x137 Munition. + + //Reactive Armor + #loc_BDArmory_part_REA_title = BD 1x0.5 Reaktivpanzerung + // A 1x0.5m section of Reactive Armor sections. Great for adding that little extra bit of protection on top of existing armor. + #loc_BDArmory_part_REA_Panel_description = Eine 1x0.5m Reaktivpanzerplatte. Bringt das kleine bisschen extra-Sicherheit zusätzlich zu existierender Panzerung. + + //Armor Panel + #loc_BDArmory_part_Panel_title = BD Panzerplatte + #loc_BDArmory_part_Panel_description = Eine haltbare Strukturplatte, die aus eine großen Auswahl von Materialien und in einer Vielzahl von Größen und Formen gefertigt werden kann. Perfekt zum konstruieren und panzern aller möglichen Sachen. + + //Tri Armor Panel + #loc_BDArmory_part_TriPanel_title = BD dreieckige Panzerplatte + #loc_BDArmory_part_Tripanel_description = Eine haltbare Strukturplatte, die aus eine großen Auswahl von Materialien und in einer Vielzahl von Größen und Formen gefertigt werden kann. Perfekt zum konstruieren und panzern aller möglichen Sachen. Dreieckig. + + //ordinance Bay + #loc_BDArmory_part_BombBay_title = Waffenschacht + #loc_BDArmory_part_BombBay_description = Ein Waffenschacht mit einem ausfahrbaren Geschützgestell. Die Nutzlast ist vom Luftstrom abgeschirmt, bis sie eingesetzt wird. + } +} diff --git a/BDArmory/Distribution/GameData/BDArmory/Localization/localization-en-us.cfg b/BDArmory/Distribution/GameData/BDArmory/Localization/localization-en-us.cfg index d864ee567..b637f2d77 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Localization/localization-en-us.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Localization/localization-en-us.cfg @@ -39,7 +39,7 @@ Localization #loc_BDArmory_part_bahaAim120_description = Medium range radar guided homing missile. //AIM-120 AMRAAM EMP Missile - #loc_BDArmory_part_bahaEMP120_title = AIM-120 AMRAAM Missile + #loc_BDArmory_part_bahaEMP120_title = AIM-120 AMRAAM EMP Missile //Medium range radar guided homing missile. #loc_BDArmory_part_bahaEMP120_description = Medium range radar guided homing missile equipped with the latest miniaturized EMP warhead. While the pulse radius is not huge (100 meters), it is quite effective. The missile does minimal structural damage, but renders all electronic devices within it's blast radius inoperable. @@ -51,7 +51,7 @@ Localization //AI Surface Operation Driver #loc_BDArmory_part_bdDriverAI_title = AI Surface Operation Driver //Flies your plane on combat air patrol missions without using your hands. Tune the values based on your plane's unique flight characteristics. Please activate engines manually. Works in conjunction with a weapon manager in guard mode (attach and configure separately). (EXPERIMENTAL) - #loc_BDArmory_part_bdDriverAI_description = Drives your car/tank/boat/etc on combat and patrol missions over the lands and seas without using your hands. Tune the values based on your ship's unique characteristics. Please activate engines manually. Works in conjunction with a weapon manager in guard mode (attach and configure separately). (EXPERIMENTAL) + #loc_BDArmory_part_bdDriverAI_description = Drives your car/tank/boat/etc on combat and patrol missions over the lands and seas without using your hands. Tune the values based on your ship's unique characteristics. Please activate engines manually. Works in conjunction with a weapon manager in guard mode (attach and configure separately). (EXPERIMENTAL) //20mm Ammunition Box #loc_BDArmory_part_baha20mmAmmo_title = 20mm Ammunition Box @@ -173,7 +173,7 @@ Localization //Goalkeeper MK2 CIWS #loc_BDArmory_part_BDAcGKmk2_title = Goalkeeper MK2 CIWS - #loc_BDArmory_part_BDAcGKmk2_description = A 7 barrel 30mm rotary cannon with full swivel range. This MK 2 version was found covered in overspray and paint cans around the back of the hangar at the old KSC, developed from the MK1 to reduce the incidence of hearing loss amongst early target pointers. This MK2 has some slight advantages over the MK1, equipped with Infra red targeting and Radar data reciever The 30x173mm high explosive rounds are only a slight improvement over the MK1 ammunition in that they at least take slightly longer to lose interest in flying and so have a good chance of reaching the target, but this weapon was never equipped to feature automatic fuse timing. + #loc_BDArmory_part_BDAcGKmk2_description = A 7 barrel 30mm rotary cannon with full swivel range. This MK 2 version was found covered in overspray and paint cans around the back of the hangar at the old KSC, developed from the MK1 to reduce the incidence of hearing loss amongst early target pointers. This MK2 has some slight advantages over the MK1, equipped with Infra red targeting and Radar data receiver The 30x173mm high explosive rounds are only a slight improvement over the MK1 ammunition in that they at least take slightly longer to lose interest in flying and so have a good chance of reaching the target, but this weapon was never equipped to feature automatic fuse timing. //TWS Locking Radar #loc_BDArmory_part_scanLockRadar1_title = TWS Locking Radar @@ -292,7 +292,7 @@ Localization //AN/APG-63 Radome #loc_BDArmory_part_bdRadome1snub_title = AN/APG-63 Radome - #loc_BDArmory_part_bdRadome1snub_description = A forward facing, aerodynamically housed radar. It can scan and lock targets within a 120 degree field of view. This is a dedicated ground attack version with much better performance against ground targets, but reduced air-to-air capabilities. + #loc_BDArmory_part_bdRadome1snub_description = A forward facing, aerodynamically housed radar. It can scan and lock targets within a 120 degree field of view. This is a dedicated ground attack version with much better performance against ground targets, but reduced air-to-air capabilities. //RBS-15 Cruise Missile #loc_BDArmory_part_bahaRBS-15Cruise_title = RBS-15 Cruise Missile @@ -339,6 +339,10 @@ Localization //A targeting pod used for targeting and surveillance. Equipped with a high resolution camera with surface and horizon stabilization, and an infrared laser for painting targets, this pod allows you to quickly find and lock grounded targets for missiles. #loc_BDArmory_part_bahaCamPod_description = A targeting pod used for targeting and surveillance. Equipped with a high resolution camera with surface and horizon stabilization, and an infrared laser for painting targets, this pod allows you to quickly find and lock grounded targets for missiles. + //AN/AAQ-42 IRST Pod + #loc_BDArmory_part_bahaIRSTPod_title = AN/AAQ-42 IRST Pod + #loc_BDArmory_part_bahaIRSTPod_description = A forward facing InfraRed Search and Track system housed in an aerodynamic pod. It can scan and detect thermal signatures within a 120 degree field of view. It is optimized for air-to-air use, and has difficulties detecting surface targets. + //Tow Launcher #loc_BDArmory_part_towLauncherTurret_title = Tow Launcher //A turret capable of holding and firing up to 4 TOW missiles. Warranty void if anything except TOW missiles are mounted. To enable the turret, select the mounted missile from the weapon manager. @@ -385,18 +389,32 @@ Localization //Reactive Armor #loc_BDArmory_part_REA_title = BD 1x0.5 Reactive Armor - #loc_BDArmory_part_Panel_description = A 1x0.5m section of Reactive Armor sections. Great for adding that little extra bit of protection on top of existing armor. + #loc_BDArmory_part_REA_Panel_description = A 1x0.5m section of Reactive Armor sections. Great for adding that little extra bit of protection on top of existing armor. //Armor Panel #loc_BDArmory_part_Panel_title = BD Armor Panel #loc_BDArmory_part_Panel_description = A sturdy Universal Structural Panel that can be configured to be a variety of sizes and use a variety of materials, perfect for constructing or armoring all sorts of things. //Tri Armor Panel - #loc_BDArmory_part_TriPanel_title = BD Armor Panel Triangle + #loc_BDArmory_part_TriPanel_title = BD Armor Panel Right Triangle + #loc_BDArmory_part_TriIsoPanel_title = BD Armor Panel Oblique Triangle #loc_BDArmory_part_Tripanel_description = A sturdy Universal Structural Panel that can be configured to be a variety of sizes and use a variety of materials, perfect for constructing or armoring all sorts of things. This one's triangular. //ordinance Bay #loc_BDArmory_part_BombBay_title = Ordinance Bay #loc_BDArmory_part_BombBay_description = A weapons bay with a deployable ordinance rack. Payload is shielded from the airstream until deployed. + + //Combat Seat + #loc_BDArmory_part_combatSeat_title = EAS-2 External Combat Seat + #loc_BDArmory_part_combatSeat_description = A command seat that contains an integrated Pilot AI and Weapons Manager to allow craft controlled by a seated Kerbal to fly your plane on combat air patrol missions without using your hands. Tune the values based on your plane's unique flight characteristics. Please activate engines manually. + + //AN/APG-77v1 ATG Radar + #loc_BDArmory_part_bdRadome1snub_ground_title = APG-77 Air To Ground Radar (Snub) + #loc_BDArmory_part_bdRadome1inline_ground_title = APG-77v1 Air To Ground Radar (Inline) + #loc_BDArmory_part_bdRadome1_ground_title = APG-77v1 Air To Ground Radar + #loc_BDArmory_part_bdRadome1_Gnd_desc = The AN/APG-77v1 is a forward facing, aerodynamically housed, solid-state, active electronically scanned array (AESA) radar. It provides full air-to-ground functionality at a MAX operating range of 40km against stationary and moving targets within a 120 degree field of view. This particular unit is optimized for Ground combat, and has difficulties locking air targets. + + #loc_BDArmory_part_AWACS_Legged = Legged + #loc_BDArmory_part_AWACS_Legless = Legless } } diff --git a/BDArmory/Distribution/GameData/BDArmory/Localization/localization-ru.cfg b/BDArmory/Distribution/GameData/BDArmory/Localization/localization-ru.cfg new file mode 100644 index 000000000..db7b7af3b --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Localization/localization-ru.cfg @@ -0,0 +1,406 @@ +Localization +{ + ru + { + #loc_BDArmory_modname = BD Armory + + #loc_BDArmory_agent_title = Bahamuto Dynamics + #loc_BDArmory_agent_description = Ведущий производитель вооружения и военного оборудования. Компания также переодически сотрудничает с космическими программами для передовых или уникальных инженерных решений. + #loc_BDArmory_part_manufacturer = Bahamuto Dynamics + + //Vulcan Turret + #loc_BDArmory_part_bahaGatlingGun_title = Турель Vulcan + //A 6 barrel 20x102mm rotary cannon. + #loc_BDArmory_part_bahaGatlingGun_description = 6-ствольная роторная пушка калибра 20x102 мм. + + //.50cal Turret + #loc_BDArmory_part_bahaTurret_title = Пулемет .50cal + //A dual barrel .50 cal machine gun. + #loc_BDArmory_part_bahaTurret_description = Двухствольный пулемет 50-го калибра. + + //USAF Airborne Laser + #loc_BDArmory_part_bahaABL_title = Бортовой Лазер USAF + //A high powered laser for setting things on fire. Uses 350 electric charge per second. + #loc_BDArmory_part_bahaABL_description = Мощный лазер, способный плавить предметы. Потребляет 350 ед. электического заряда в секунду. + + //Adjustable Missile Rail + #loc_BDArmory_part_bahaAdjustableRail_title = Регулируемая Ракетная Направляющая + //A rail for mounting missiles. + #loc_BDArmory_part_bahaAdjustableRail_description = Направляющая для установки ракет. + + //AGM-86C Cruise Missile + #loc_BDArmory_part_bahaAgm86B_title = Крылатая Ракета AGM-86C + //Long distance, sub-sonic, air-launched, GPS-guided cruise missile. This missile has no booster, so it must be launched while airborne at cruising speed. + #loc_BDArmory_part_bahaAgm86B_description = Дозвуковая крылатая ракета дальнего действия воздушного базирования с GPS-наведением. У этой ракеты нет ускорителей, так что она должна быть запущена в воздухе на крейсерской скорости. + + //AIM-120 AMRAAM Missile + #loc_BDArmory_part_bahaAim120_title = Ракета AIM-120 AMRAAM + //Medium range radar guided homing missile. + #loc_BDArmory_part_bahaAim120_description = Самонаводящаяся ракета средней дальности с радиолокационным наведением. + + //AIM-120 AMRAAM EMP Missile + #loc_BDArmory_part_bahaEMP120_title = Ракета AIM-120 AMRAAM EMP + //Medium range radar guided homing missile. + #loc_BDArmory_part_bahaEMP120_description = Самонаводящаяся ракета средней дальности с радиолокационным наведением, оснащенная новейшей миниатюрной ЭМИ-боеголовкой. Имея небольшой радиус импульса (100 метров), она достаточно эффективна. Эта ракета наносит минимальный физический ущерб, но выводит из строя всю электронику в радиусе действия. + + //AI Pilot Flight Computer + #loc_BDArmory_part_bdPilotAI_title = ИИ Автопилот + //Flies your plane on combat air patrol missions without using your hands. Tune the values based on your plane's unique flight characteristics. Please activate engines manually. Works in conjunction with a weapon manager in guard mode (attach and configure separately). (EXPERIMENTAL) + #loc_BDArmory_part_bdPilotAI_description = Управляет вашим самолетом во время боевых вылетов. Настройте значения, основываясь на уникальных ЛТХ вашего самолета. Пожалуйста, активируйте двигатели вручную. Работает совместно с контроллером вооружения в боевом режиме (присоединяется и настраивается отдельно). (EXPERIMENTAL) + + //AI Surface Operation Driver + #loc_BDArmory_part_bdDriverAI_title = ИИ Автопилот (Поверхность) + //Flies your plane on combat air patrol missions without using your hands. Tune the values based on your plane's unique flight characteristics. Please activate engines manually. Works in conjunction with a weapon manager in guard mode (attach and configure separately). (EXPERIMENTAL) + #loc_BDArmory_part_bdDriverAI_description = Управляет вашей машиной, танком, кораблем и т.д. на боевых миссиях на земле и на воде. Настройте значения, основываясь на уникальных характеристиках вашего аппарата. Пожалуйста, активируйте двигатели вручную. Работает совместно с контроллером вооружения в боевом режиме (присоединяется и настраивается отдельно). (EXPERIMENTAL) + + //20mm Ammunition Box + #loc_BDArmory_part_baha20mmAmmo_title = 20мм Ящик Боеприпасов + //Ammo box containing 650 20x102mm rounds. + #loc_BDArmory_part_baha20mmAmmo_description = Ящик с боеприпасами, содержащий 650 патронов калибра 20х102 мм. + //25mm Ammunition Box + #loc_BDArmory_part_baha25mmAmmo_title = 25мм Ящик Боеприпасов + //Ammo box containing 650 20x102mm rounds. + #loc_BDArmory_part_baha25mmAmmo_description = Ящик с боеприпасами, содержащий 625 патронов калибра 25х137 мм. + + //30mm Ammunition Box + #loc_BDArmory_part_baha30mmAmmo_title = 30мм Ящик Боеприпасов + //Ammo box containing 600 30x173mm rounds. + #loc_BDArmory_part_baha30mmAmmo_description = Ящик с боеприпасами, содержащий 600 патронов калибра 30x173 мм. + + //70mm Rocket Ammunition Box + #loc_BDArmory_part_rocket70mmAmmo_title = 70мм Ракетный Ящик Боеприпасов + //Ammo box containing 48 70mm Rockets. + #loc_BDArmory_part_rocket70mmAmmo_description = Ящик с боеприпасами, содержащий 48 ракет калибра 70 мм. + + //50cal Ammunition Box + #loc_BDArmory_part_baha50CalAmmo_title = 50cal Ящик Боеприпасов + //Ammo box containing 1200 .50 cal rounds. + #loc_BDArmory_part_baha50CalAmmo_description = Ящик с боеприпасами, содержащий 1200 патронов .50 cal. + + //Universal Ammo Box (Legacy) + #loc_BDArmory_part_UniversalAmmoBoxBDA_title = Универсальный Ящик Боеприпасов (Legacy) + //(Obsolete - DO NOT USE - Requires Fire Spitter) Scalable Ammo box containing whatever ammo you want to put in it, does hold a selectable quantity of every ammunition type up to 16'1 inch that is currently used in KSP in association with BDAc Extra types can be added upon request (no fantasy ammo please) NOTE: this part still requires Fire Spitter, and is here for backwards compatability. Use the new UniversalAmmo part going forward. + #loc_BDArmory_part_UniversalAmmoBoxBDA_description = (Устарел - НЕ РЕКОМЕНДУЕТСЯ К ИСПОЛЬЗОВАНИЮ - Требуется Fire Spitter) Изменяемая коробка с боеприпасами, содержащая любые боеприпасы, которые вы хотите в нее положить, содержит выбранное количество каждого типа боеприпасов до 16'1 дюйма, который в настоящее время используется в KSP совместно с BDA. Дополнительные типы могут быть добавлены по запросу (пожалуйста, без вымышленных патронов) ПРИМЕЧАНИЕ: для этой части по-прежнему требуется FireSpitter, и она приведена здесь для обеспечения обратной совместимости. В дальнейшем используйте новый универсальный ящик боеприпасов. + + //Universal Ammo Box + #loc_BDArmory_part_BDAcUniversalAmmoBox_title = Универсальный Ящик Боеприпасов + //Scaleable Ammo box containing whatever ammo you want to put in it, does hold a selectable quantity of every ammunition type up to 16'1 inch that is currently used in KSP in association with BDAc Extra types can be added upon request (no fantasy ammo please) + #loc_BDArmory_part_BDAcUniversalAmmoBox_description = Изменяемая коробка с боеприпасами, содержащая любые боеприпасы, которые вы хотите в нее положить, содержит выбранное количество каждого типа боеприпасов до 16'1 дюйма, который в настоящее время используется в KSP совместно с BDA. Дополнительные типы могут быть добавлены по запросу (пожалуйста, без вымышленных патронов) + + //Cannon Ammunition Box + #loc_BDArmory_part_bahaCannonShellBox_title = Ящик Артиллерийских Боеприпасов + //Ammo box containing 10 cannon shells. + #loc_BDArmory_part_bahaCannonShellBox_description = Ящик с боеприпасами, содержащий 10 артиллерийских снарядов. + + //BD 1x1 slope Armor + #loc_BDArmory_part_BD1x1slopeArmor_title = Наклонная Броня BD 1x1 + //A sturdy 1x1 slope Armor plate, perfect for constructing all sorts of things. PS does not float + #loc_BDArmory_part_BD1x1slopeArmor_description = Прочная наклонная бронированная панель 1х1, идеально подходящая для строительства всевозможных объектов. PS не плавает + + //BD 2x1 slope Armor + #loc_BDArmory_part_BD2x1slopeArmor_title = Наклонная Броня BD 2x1 + //A sturdy 2x1 slope Armor plate, perfect for constructing all sorts of things. PS does not float + #loc_BDArmory_part_BD2x1slopeArmor_description = Прочная наклонная бронированная панель 2х1, идеально подходящая для строительства всевозможных объектов. PS не плавает + + //BD 1x1 panel Armor + #loc_BDArmory_part_BD1x1panelArmor_title = Бронированная панель BD 1x1 + //A sturdy 1x1 Armor plate, perfect for constructing all sorts of things. PS does not float + #loc_BDArmory_part_BD1x1panelArmor_description = Прочная бронированная панель 1х1, идеально подходящая для строительства всевозможных объектов. PS не плавает + + //BD 2x1 panel Armor + #loc_BDArmory_part_BD2x1panelArmor_title = Бронированная панель BD 2x1 + //A sturdy 2x1 Armor plate, perfect for constructing all sorts of things. PS does not float + #loc_BDArmory_part_BD2x1panelArmor_description = Прочная бронированная панель 2х1, идеально подходящая для строительства всевозможных объектов. PS не плавает + + //BD 3x1 panel Armor + #loc_BDArmory_part_BD3x1panelArmor_title = Бронированная панель BD 3x1 + //A sturdy 3x1 Armor plate, perfect for constructing all sorts of things. PS does not float + #loc_BDArmory_part_BD3x1panelArmor_description = Прочная бронированная панель 3х1, идеально подходящая для строительства всевозможных объектов. PS не плавает + + //BD 4x1 panel Armor + #loc_BDArmory_part_BD4x1panelArmor_title = Бронированная панель BD 4x1 + //A sturdy 4x1 Armor plate, perfect for constructing all sorts of things. PS does not float + #loc_BDArmory_part_BD4x1panelArmor_description = Прочная бронированная панель 4х1, идеально подходящая для строительства всевозможных объектов. PS не плавает + + //AWACS Detection Radar + #loc_BDArmory_part_awacsRadar_title = Радар Обнаружения AWACS + //A large radar capable of detecting objects from a long distance. This radar does NOT have the capability of tracking or locking targets. + #loc_BDArmory_part_awacsRadar_description = Большой радар, способный обнаруживать объекты на большом расстоянии. Этот радар не имеет возможности сопровождать или захватывать цели. + + //Modular Missile Guidance (EXPERIMENTAL) + #loc_BDArmory_part_bdammGuidanceModule_title = Наведение Модульной Ракеты (EXPERIMENTAL) + //A missile guidance computer. Manually tune steering settings to craft's unique flight characteristics. Select a guidance mode. Select a target then enable guidance. Activate engines and stages manually. (EXPERIMENTAL) + #loc_BDArmory_part_bdammGuidanceModule_description = Компьютер наведения ракеты. Вручную настройте рулевое управление, основываясь на уникальных характеристиках ракеты. Выберите тип наведения. Выберите цель, затем включите наведение. Активируйте двигатели и ступени вручную. (EXPERIMENTAL) + + //Browning .50cal AN/M2 + #loc_BDArmory_part_bahaBrowningAnm2_title = .50cal AN/M3 Браунинга + //An old fixed .50 cal machine gun 50cal ammo + #loc_BDArmory_part_bahaBrowningAnm2_description = Старый .50 cal пулемет. + + //CBU-87 Cluster Bomb + #loc_BDArmory_part_bahaClusterBomb_title = Кластерная Бомба CBU-87 + //This bomb splits open and deploys many small bomblets at a certain altitude. + #loc_BDArmory_part_bahaClusterBomb_description = Эта бомба раскрывается и выпускает множество маленьких снарядов на определенной высоте. + + //Chaff Dispenser + #loc_BDArmory_part_bahaChaffPod_title = Раздатчик Дипольных Отражателей + //Drops chaff for confusing or breaking radar locks. + #loc_BDArmory_part_bahaChaffPod_description = Сбрасывает отражатели для запутывания ракет с радиолокационным наведением. + + //Flare Dispenser + #loc_BDArmory_part_bahaCmPod_title = Раздатчик Тепловых Ловушек + //Drops flares for confusing heat-seeking missiles. + #loc_BDArmory_part_bahaCmPod_description = Сбрасывает ловушки для запутывания ракет с тепловым наведением. + + //AN/ALQ-131 ECM Jammer + #loc_BDArmory_part_bahaECMJammer_title = AN/ALQ-131 РЭБ-Глушитель + //This electronic device makes it harder for radars to lock onto your vehicle, and increases your chances of breaking the lock. + #loc_BDArmory_part_bahaECMJammer_description = Это электронное устройство затрудняет радарное наведение на ваш транспорт и увеличивает ваши шансы сбросить захват. + + //GAU-8 30x173mm Cannon + #loc_BDArmory_part_bahaGau-8_title = GAU-8 30x173мм Пулемет + //A 7 barrel 30mm rotary cannon. + #loc_BDArmory_part_bahaGau-8_description = 7-ствольная 30-мм роторный пулемет. + + //Goalkeeper CIWS + #loc_BDArmory_part_bahaGoalKeeper_title = Goalkeeper CIWS + #loc_BDArmory_part_bahaGoalKeeper_description = 7-ствольная 30-мм поворотная пушка с полным диапазоном поворота. 30-миллиметровые осколочно-фугасные снаряды самоподрываются на заданном расстоянии, но это оружие не имеет функции автоматического выбора времени срабатывания предохранителя. У него есть свой собственный радар обнаружения и слежения, но он эффективен только на ближнем расстоянии и не заменяет надлежащий поисковый радар. + + //GoalkeeperMk1 CIWS + #loc_BDArmory_part_GoalKeeperBDAcMk1_title = Goalkeeper Mk1 CIWS + #loc_BDArmory_part_GoalKeeperBDAcMk1_description = 7-ствольная 30-мм поворотная пушка с полным диапазоном поворота. Версию MK1 мы нашли под брезентом в поле. Идеально подходит для стесненных в средствах ополченцев и изворотливых правительств (версия для скупердяев) Не имея радара, эта турель требует получение информации о цели из альтернативного источника (кто-то, указывающий пальцем и кричащий "стреляй в это", оказался малоэффективным из-за чрезмерного шума, производимого при стрельбе из оружия). 30-миллиметровые осколочно-фугасные снаряды самоподрываются на заданном расстоянии, но это оружие не имеет функции автоматического выбора времени срабатывания предохранителя. + + //Goalkeeper MK2 CIWS + #loc_BDArmory_part_BDAcGKmk2_title = Goalkeeper MK2 CIWS + #loc_BDArmory_part_BDAcGKmk2_description = 7-ствольная 30-мм поворотная пушка с полным диапазоном поворота. Версию MK2 мы нашли в спрее и банках из-под краски за ангаром в старом космическом центре. Разработан на основе MK1 для снижения частоты потери отклика у ранних целеуказателей. MK2 имеет некоторые небольшие преимущества по сравнению с MK1, оснащен инфракрасным прицеливанием и приемником радиолокационных данных. Осколочно-фугасные патроны 30x173 мм являются лишь небольшим улучшением по сравнению с боеприпасами MK1 в том смысле, что им, по крайней мере, требуется немного больше времени, чтобы потерять интерес к полету, и поэтому у них есть хорошие шансы достичь цели, но это оружие никогда не оснащалось функцией автоматического выбора времени срабатывания взрывателя. + + //TWS Locking Radar + #loc_BDArmory_part_scanLockRadar1_title = Захватывающий Радар TWS + #loc_BDArmory_part_scanLockRadar1_description = Это устройство оснащено радаром обнаружения средней дальности и встроенным радаром сопровождения цели. Этот радар способен захватывать цели и будет продолжать сканирование во время сопровождения захваченной цели (TWS - Track While Scan). Он оптимизирован для воздушного поиска и сопровождения и испытывает трудности с обнаружением и сопровождением наземных целей. + + //Large Detection Radar + #loc_BDArmory_part_scanLargeRadar_title = Большой Радар Обнаружения + #loc_BDArmory_part_scanLargeRadar_description = Большой радар, способный обнаруживать объекты на большом расстоянии. Этот радар не имеет возможности сопровождать или захватывать цели. Он оптимизирован для воздушного поиска и испытывает трудности с обнаружением наземных целей. + + //F-86 Launcher + #loc_BDArmory_part_F86RL_title = Перезаряжаемая Ракетная Установка FFAR + #loc_BDArmory_part_F86RL_description = Встраиваемая ракетная установка, предназначенная для использования в режиме "Воздух-воздух". Вмещает 24 неуправляемые воздушные ракеты со складывающимися килями. Может быть перезаряжен из ящика боеприпасов, если ракеты закончатся. + + //Hydra-70 Rocket Pod + #loc_BDArmory_part_bahaH70Launcher_title = Ракетная Установка Hydra-70 + //Holds and fires 19 unguided Hydra-70 rockets. + #loc_BDArmory_part_bahaH70Launcher_description = Вмещает и запускает 19 неуправляемых ракет Hydra-70. + + //Hydra-70 Rocket Turret + #loc_BDArmory_part_bahaH70Turret_title = Ракетная Турель Hydra-70 + //Turret pod that holds and fires 32 unguided Hydra-70 rockets. + #loc_BDArmory_part_bahaH70Turret_description = Турельный отсек, вмещающий и выпускающий 32 неуправляемые ракеты Hydra-70. + + //AGM-88 HARM Missile + #loc_BDArmory_part_bahaHarm_title = Ракета AGM-88 HARM + //High-speed anti-radiation missile. This missile will home in on radar sources detected by the Radar Warning Receiver. + #loc_BDArmory_part_bahaHarm_description = Высокоскоростная противорадиационная ракета. Эта ракета нацелится на источники радиоволн, обнаруженные приемником радиолокационного предупреждения. + + //HE-KV-1 Missile + #loc_BDArmory_part_bahaHEKV1_title = Ракета HE-KV-1 + //The HE-KV-1 (High explosive kill vehicle) is a radar-guided homing missile that uses reaction control thrusters and thrust vectoring to maneuver. This means it is capable of steering towards targets in a vacuum. + #loc_BDArmory_part_bahaHEKV1_description = HE-KV-1 (High explosive kill vehicle) это самонаводящаяся ракета с радиолокационным наведением, которая использует реактивную систему управления и изменяемый вектор тяги для маневрирования. Это означает, что она способна направляться на цели в вакууме. + + //AGM-114 Hellfire Missile + #loc_BDArmory_part_bahaAGM-114_title = Ракета AGM-114 Hellfire + //Small, quick, laser guided homing missile. + #loc_BDArmory_part_bahaAGM-114_description = Маленькая, быстрая самонаводящаяся ракета с лазерным наведением. + + //AGM-114 Hellfire II EMP Missile + #loc_BDArmory_part_bahaAGM-114_EMP_title = AGM-114R Hellfire II EMP + //Small, quick, laser guided homing missile. + #loc_BDArmory_part_bahaAGM-114_EMP_description = Маленькая, быстрая самонаводящаяся ракета с лазерным наведением, оснащенная новейшей миниатюрной ЭМИ-боеголовкой. Имея маленький радиус импульса (50 метров), она достаточно эффективна. Эта ракета наносит минимальный физический ущерб, но выводит из строя всю электронику в радиусе действия. + + //Vulcan (Hidden) + #loc_BDArmory_part_bahaHiddenVulcan_title = Vulcan (Скрытый) + //A 6 barrel 20x102mm rotary cannon. 20x102Ammo + #loc_BDArmory_part_bahaHiddenVulcan_description = 6-ствольная роторная пушка калибра 20х102 мм. + + //Mk83 JDAM Bomb + #loc_BDArmory_part_bahaJdamMk83_title = Бомба Mk83 JDAM + //1000lb GPS-guided bomb. + #loc_BDArmory_part_bahaJdamMk83_description = 1000-килограммовая бомба с GPS-наведением. + + //M102 Howitzer (Radial) + #loc_BDArmory_part_bahaM102Howitzer_title = M102 Howitzer (Радиальный) + //A radially mounted 105mm gun. CannonShells + #loc_BDArmory_part_bahaM102Howitzer_description = Радиально установленное 105-мм орудие. + + //M1 Abrams Cannon + #loc_BDArmory_part_bahaM1Abrams_title = Башня M1 Abrams + //A 120mm cannon on an armored turret. CannonShells + #loc_BDArmory_part_bahaM1Abrams_description = 120-мм орудие на бронированной башне. + + //M230 Chain Gun Turret + #loc_BDArmory_part_bahaM230ChainGun_title = Ленточный Пулемет M230 + //The M230 Chain Gun is a single-barrel automatic cannon firing 30x173 Ammo high explosive rounds. It is commonly used on attack helicopters. + #loc_BDArmory_part_bahaM230ChainGun_description = Ленточный пулемет M230 представляет собой одноствольную автоматическую пушку, стреляющую осколочно-фугасными снарядами 30х173 калибра. Он обычно используется на ударных вертолетах. + + //AGM-65 Maverick Missile + #loc_BDArmory_part_bahaAGM-65_title = Ракета AGM-65 Maverick + //Medium yield laser guided air-to-ground missile. + #loc_BDArmory_part_bahaAGM-65_description = Ракета класса "воздух-земля" с лазерным наведением средней мощности. + + //Jernas Missile Turret + #loc_BDArmory_part_missileTurretTest_title = Ракетная Турель Jernas + //A turret capable of holding and firing up to 8 small to medium sized missiles. Comes with an integrated detection and tracking radar. Warranty void if anything except missiles are mounted. To enable the turret, select the mounted missile from the weapon manager. + #loc_BDArmory_part_missileTurretTest_description = Башня, способная вмещать и выпускать до 8 ракет малого и среднего размера. Поставляется со встроенным радаром обнаружения и сопровождения. Гарантия аннулируется, если будет установлено что-либо, кроме ракет. Чтобы активировать башню, выберите установленную ракету в контроллере вооружения. + + //Mk82 Bomb + #loc_BDArmory_part_bahaMk82Bomb_title = Бомба Mk82 + //500lb unguided bomb. + #loc_BDArmory_part_bahaMk82Bomb_description = 500-килограммовая неуправляемая бомба. + + //Mk82 SnakeEye Bomb + #loc_BDArmory_part_bahaMk82BombBrake_title = Бомба Mk82 SnakeEye + //500lb unguided bomb with airbrakes. Use for low altitude bombing. + #loc_BDArmory_part_bahaMk82BombBrake_description = 500-килограммовая неуправляемая бомба с аэротормозами. Используется для бомбометания на малой высоте. + + //Oerlikon Millennium Cannon + #loc_BDArmory_part_bahaOMillennium_title = Пушка Тысячелетия Эрликона + //A turret that fires timed detonation explosive rounds. Suited for close-in air defense. A device at the muzzle end of the barrel measures the exact speed of each round as it is fired, and automatically sets the fuse to detonate the round as it approaches a pre-set distance from the target. Uses 30x173Ammo + #loc_BDArmory_part_bahaOMillennium_description =Турель, которая стреляет разрывными снарядами с временной детонацией. Подходит для ближней противовоздушной обороны. Устройство на дульном конце ствола измеряет точную скорость каждого выстрела и автоматически устанавливает предохранитель для детонации снаряда, когда он приближается к заданному расстоянию от цели. Использует снаряды 30x173 калибра. + + //PAC-3 Intercept Missile + #loc_BDArmory_part_bahaPac-3_title = Ракета-Перехватчик PAC-3 + //Medium range, high speed, radar-guided surface to air missile. + #loc_BDArmory_part_bahaPac-3_description = Высокоскоростная ракета класса "земля-воздух" средней дальности с радарным наведением. + + //Patriot Launcher Turret + #loc_BDArmory_part_patriotLauncherTurret_title = Пусковая Установка Системы Patriot + //A turret capable of holding and firing up to 16 PAC-3 missiles (4 per cannister). Warranty void if anything except missiles are mounted. To enable the turret, select the mounted missile from the weapon manager. + #loc_BDArmory_part_patriotLauncherTurret_description = Башня, способная вмещать и выпускать до 16 ракет PAC-3 (по 4 на канистру). Гарантия аннулируется, если будет установлено что-либо, кроме ракет. Чтобы активировать башню, выберите установленную ракету в контроллере вооружения. + + //Radar Data Receiver + #loc_BDArmory_part_radarDataReceiver_title = Приемник Радиолокационного Предупреждения + //A module that can display radar contacts via data-link and lock targets through a remote radar, but can not scan or lock by itself. Useful for a hidden missile battery. + #loc_BDArmory_part_radarDataReceiver_description = Модуль, который может отображать контакты радара по каналу передачи данных и захватывать цели с помощью удаленного радара, но не может сканировать или захватывать самостоятельно. Полезно для скрытой ракетной батареи. + + //AN/APG-63 Radome + #loc_BDArmory_part_bdRadome1_title = Обтекатель AN/APG-63 + #loc_BDArmory_part_bdRadome1_description = Направленный вперед аэродинамически размещенный радар. Он может сканировать и захватывать цели в пределах 120-градусного поля зрения. Он оптимизирован для ведения воздушного боя и испытывает трудности с захватом наземных целей. + + //AN/APG-63 Inline Radome + #loc_BDArmory_part_bdRadome1inline_title = Встроенный Обтекатель AN/APG-63 + #loc_BDArmory_part_bdRadome1inline_description = Направленный вперед аэродинамически размещенный радар. Он может сканировать и захватывать цели в пределах 120-градусного поля зрения. Убедитесь, что черные метки направлены вперед. Он оптимизирован для ведения воздушного боя и испытывает трудности с захватом наземных целей. + + //AN/APG-63 Radome + #loc_BDArmory_part_bdRadome1snub_title = Короткий Обтекатель AN/APG-63 + #loc_BDArmory_part_bdRadome1snub_description = Направленный вперед аэродинамически размещенный радар. Он может сканировать и захватывать цели в пределах 120-градусного поля зрения. Это специальная версия для атаки наземных целей, но уменьшенными возможностями в воздушном бою. + + //RBS-15 Cruise Missile + #loc_BDArmory_part_bahaRBS-15Cruise_title = Крылатая Ракета RBS-15 + //Long distance, multi-platform high-speed cruise missile with boosters. + #loc_BDArmory_part_bahaRBS-15Cruise_description = Многоплатформенная высокоскоростная крылатая ракета большой дальности с ускорителями. + + //RBS-15 Air launched Cruise Missile + #loc_BDArmory_part_bahaRBS-15ALCruise_title = Крылатая Ракета RBS-15 Воздушного Базирования + //Long distance, multi-platform high-speed cruise missile with boosters. + #loc_BDArmory_part_bahaRBS-15ALCruise_description = Многоплатформенная высокоскоростная крылатая ракета дальнего действия воздушного базирования без внешних ускорителей. + + //Adjustable Rotary Bomb Rack + #loc_BDArmory_part_bdRotBombBay_title = Регулируемая Поворотная Бомбовая Стойка + //An adjustable rotary bomb rack. The yellow arrow should be pointing in the direction of weapon release. Missiles or bombs only. One per rail only. + #loc_BDArmory_part_bdRotBombBay_description = Регулируемая поворотная бомбовая стойка. Желтая стрелка должна указывать в направлении запуска оружия. Только ракеты или бомбы. Только по одной на направляющую. + + //S-8KOM Rocket Pod + #loc_BDArmory_part_bahaS-8Launcher_title = Ракетная Установка S-8KOM + //Holds and fires 23 unguided S-8KOM rockets. It has an aerodynamic nose cone. + #loc_BDArmory_part_bahaS-8Launcher_description = Вмещает и запускает 23 неуправляемые ракеты S-8KOM. Имеет аэродинамический носовой обтекатель. + + //AIM-9 Sidewinder Missile + #loc_BDArmory_part_bahaAim9_title = Ракета AIM-9 Sidewinder + //Short range heat seeking missile. + #loc_BDArmory_part_bahaAim9_description = Ракета малой дальности с тепловым наведением. + + //Small High Explosive Warhead + #loc_BDArmory_part_bdWarheadSmall_title = Малая Фугасная Боеголовка + //A missile nose cone packed with explosives. + #loc_BDArmory_part_bdWarheadSmall_description = Носовой обтекатель ракеты, начиненный взрывчаткой. + + //Smoke Countermeasure Pod + #loc_BDArmory_part_bahaSmokeCmPod_title = Модуль Дымовой Завесы + //Fires smoke-screen countermeasures for occluding laser points. + #loc_BDArmory_part_bahaSmokeCmPod_description = Активирует дымовую завесу для блокирования лазерного облучения. + + //FLIR Targeting Ball + #loc_BDArmory_part_bahaFlirBall_title = Шар Наведения FLIR + //A ball camera used for targeting and surveillance. Equipped with a high resolution camera with surface and horizon stabilization, and an infrared laser for painting targets, this pod allows you to quickly find and lock grounded targets for missiles. + #loc_BDArmory_part_bahaFlirBall_description = Шаровая камера, используемая для наведения на цель и наблюдения. Оснащенный камерой высокого разрешения и системой стабилизации, а также инфракрасным лазером для помечения целей, этот модуль позволяет быстро находить и захватывать наземные цели для ракет. + + //AN/AAQ-28 Targeting Pod + #loc_BDArmory_part_bahaCamPod_title = Модуль Наведения AN/AAQ-28 + //A targeting pod used for targeting and surveillance. Equipped with a high resolution camera with surface and horizon stabilization, and an infrared laser for painting targets, this pod allows you to quickly find and lock grounded targets for missiles. + #loc_BDArmory_part_bahaCamPod_description = Модуль наведения, используемый для наведения на цель и наблюдения. Оснащенный камерой высокого разрешения и системой стабилизации, а также инфракрасным лазером для помечения целей, этот модуль позволяет быстро находить и захватывать наземные цели для ракет. + + //AN/AAQ-42 IRST Pod + #loc_BDArmory_part_bahaIRSTPod_title = Инфракрасный Модуль AN/AAQ-42 + #loc_BDArmory_part_bahaIRSTPod_description = Направленная вперед инфракрасная система поиска и сопровождения, размещенная в аэродинамической капсуле. Может сканировать и обнаруживать тепловые сигнатуры в пределах 120-градусного поля зрения. Он оптимизирован для поиска воздушных целей и испытывает трудности с обнаружением наземных целей. + + //Tow Launcher + #loc_BDArmory_part_towLauncherTurret_title = Пусковая Установка TOW + //A turret capable of holding and firing up to 4 TOW missiles. Warranty void if anything except TOW missiles are mounted. To enable the turret, select the mounted missile from the weapon manager. + #loc_BDArmory_part_towLauncherTurret_description = Турель, способная вмещать и выпускать до 4 ракет TOW. Гарантия аннулируется, если установлено что-либо, кроме ракет TOW. Чтобы активировать турель, выберите установленную ракету в контроллере вооружения. + + //BGM-71 Tow Missile + #loc_BDArmory_part_bahaTowMissile_title = Ракета BGM-71 TOW + //Short distance, laser beam-riding, wireless anti-tank missile. + #loc_BDArmory_part_bahaTowMissile_description = Противотанковая ракета малой дальности с лазерным наведением. + + //Weapon Manager + #loc_BDArmory_part_missileController_title = Контроллер Вооружения + //Cycle through missiles/bombs and fire them with a single button. + #loc_BDArmory_part_missileController_description = Переключайтесь между ракетами/бомбами и запускайте их с помощью одной кнопки. + + //BDAsonarPod1A + #loc_BDArmory_part_BDAsonarPod1A_title = Сонар BDA MK1 + #loc_BDArmory_part_BDAsonarPod1A_description = Сонар BDA MK1 может обнаруживать только подводные объекты. Как встроенный сонар, он имеет ограниченную дальность действия и чувствительность. + + //StingRayBDATorpedo + #loc_BDArmory_part_StingRayBDATorpedo_title = Легкая Торпеда Sting Ray BDA + #loc_BDArmory_part_StingRayBDATorpedo_description = Легкая Торпеда Sting Ray предназначена для запуска с корабля и сброса с вертолета, не используются на подводных лодках. Интересный факт, вы можете поместить 16 таких в пусковую установку, хотя использование их в таком устройстве без надлежащей подготовки стало причиной большого количества слез и писем начальству. + + //SaturnAL31 + #loc_BDArmory_part_SaturnAL31_title = Турбореактивный Двигатель АЛ-31ФМ1 «Сатурн» + //A high performance jet engine with a variable geometry thrust vectoring nozzle and an afterburner for extra thrust. Based on the highly popular J-404 engine, KTech engineers saw the potential of (highly) modifying the commercial variant into a formidable powerplant for military use. After seeing the potential of the engine, the BDAc group immediately licensed it for their new MkIII test drone. + #loc_BDArmory_part_SaturnAL31_description = Высокоэффективный реактивный двигатель с изменяемым вектором тяги и форсажной камерой. Основываясь на популярном двигателе J-404, инженеры KTech увидели потенциал в (значительной) модификации коммерческого варианта в мощную силовую установку для военного использования. Заметив потенциал двигателя, BDAc group немедленно лицензировала его для своего нового тестового дрона MkIII. + + //GravityGun + #loc_BDArmory_part_GravGun_title = Энергетический Манипулятор Нулевых Колебаний + #loc_BDArmory_part_GravGun_description = Очень высокотехнологичная пушка, которая превратила гравитацию в оружие. По-видимому, это оружие, основанное на сочетании техно-магии и инопланетных технологий, способно воздействовать на цели неньютоновскими силами, а также влиять на их массу. Bahamuto Dynamics не несет ответственности за любой ущерб (вам, имуществу или структуре реальности), причиненный этим оружием. + + //Genie + #loc_BDArmory_part_genie_title = Ракета Воздух-Воздух AIR-2 Genie + #loc_BDArmory_part_genie_description = Ядерная противовоздушная ракета мощностью 1,5 кт. + + //GAU-22 + #loc_BDArmory_part_GAU22_title = Пулемет GAU-22/A 25x137мм + #loc_BDArmory_part_GAU22_description = 4-ствольная 25-мм роторная пушка. + + //Sidam + #loc_BDArmory_part_sidam_title = Зенитное Орудие Sidam + #loc_BDArmory_part_sidam_description = 25-миллиметровое зенитное орудие для залпового огня. + + //Reactive Armor + #loc_BDArmory_part_REA_title = Динамическая Защита BD 1x0.5 + #loc_BDArmory_part_REA_Panel_description = Модуль динамической защиты размером 1x0,5 м. Отлично подходит для небольшой дополнительной защиты поверх существующей брони. + + //Armor Panel + #loc_BDArmory_part_Panel_title = Бронированная Панель BD + #loc_BDArmory_part_Panel_description = Прочная универсальная структурная панель, которая может быть сконфигурирована под различные размеры и материалы, идеально подходит для конструирования или бронирования всевозможных вещей. + + //Tri Armor Panel + #loc_BDArmory_part_TriPanel_title = Треугольная Бронированная Панель BD + #loc_BDArmory_part_TriIsoPanel_title = Треугольная Бронированная Панель BD + #loc_BDArmory_part_Tripanel_description = Прочная универсальная структурная панель, которая может быть сконфигурирована под различные размеры и материалы, идеально подходит для конструирования или бронирования всевозможных вещей. Эта треугольная. + + //ordinance Bay + #loc_BDArmory_part_BombBay_title = Оружейный Отсек + #loc_BDArmory_part_BombBay_description = Выдвигаемый отсек для хранения оружия. Полезная нагрузка защищена от воздушного потока до момента развертывания. + } +} diff --git a/BDArmory/Distribution/GameData/BDArmory/MMPatches/000000_HitpointModule_PartFixes.cfg b/BDArmory/Distribution/GameData/BDArmory/MMPatches/000000_HitpointModule_PartFixes.cfg index cc6a3612c..c5d70bcc3 100644 --- a/BDArmory/Distribution/GameData/BDArmory/MMPatches/000000_HitpointModule_PartFixes.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/MMPatches/000000_HitpointModule_PartFixes.cfg @@ -604,6 +604,25 @@ ExplodeMode = Never } } +@PART[M2X_TurbofanMk2] //JE-1 Mule +{ + %MODULE[HitpointTracker] + { + ArmorThickness = 10 + maxHitPoints = 2500 //clamping from 36k + ExplodeMode = Never + } +} + +@PART[M3X_Turbofan] //JE-4 Buffalo +{ + %MODULE[HitpointTracker] + { + ArmorThickness = 10 + maxHitPoints = 4000 //claming from 52k + ExplodeMode = Never + } +} @PART[M2X_EngineShroud] //Mk2 Engine Shroud { @@ -740,7 +759,7 @@ %MODULE[HitpointTracker] { ArmorThickness = 10 - maxHitPoints = 700 //Buff, original was 100, balanced around Big-S Delta Wing + maxHitPoints = 2400 //Buff, original was 100, balanced around Big-S Delta Wing ExplodeMode = Never } } @@ -2573,6 +2592,14 @@ armorVolume = 33.75 } } +@PART[parachuteSingle] +{ + %MODULE[HitpointTracker] + { + ExplodeMode = Never + maxHitPoints = 150 + } +} /////////////////////////////////////////// //Fixes for AP+ Drones and SAS @@ -2935,7 +2962,7 @@ %MODULE[HitpointTracker] { ArmorThickness = 10 - maxHitPoints = 1000 //Buff, original was 800, changed to be slightly more viable + maxHitPoints = 4680 //Buff, original was 1000!?, changed to be slightly more viable ExplodeMode = Never armorVolume = 42 } @@ -3460,7 +3487,7 @@ %MODULE[HitpointTracker] { ArmorThickness = 10 - maxHitPoints = 2000 //Buff, original was 800, it's bloody massive and only had 800!? + maxHitPoints = 4680 //Buff, used to be 2K ExplodeMode = Never armorVolume = 64 } @@ -3946,3 +3973,30 @@ ExplodeMode = Never } } +@PART[bahaRocketBox] //AmmoBox +{ + %MODULE[HitpointTracker] + { + ArmorThickness = 10 + maxHitPoints = 200 //buff, original was 100, buffed on request + ExplodeMode = Never + } +} +@PART[baha25mmAmmo] //AmmoBox +{ + %MODULE[HitpointTracker] + { + ArmorThickness = 10 + maxHitPoints = 200 //buff, original was 100, buffed on request + ExplodeMode = Never + } +} +@PART[UniversalAmmoBoxBDA] //AmmoBox +{ + %MODULE[HitpointTracker] + { + ArmorThickness = 10 + maxHitPoints = 200 //buff, original was 100, buffed on request + ExplodeMode = Never + } +} diff --git a/BDArmory/Distribution/GameData/BDArmory/MMPatches/001_ClawExtensions.cfg b/BDArmory/Distribution/GameData/BDArmory/MMPatches/001_ClawExtensions.cfg new file mode 100644 index 000000000..7072d46d6 --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/MMPatches/001_ClawExtensions.cfg @@ -0,0 +1,7 @@ +@PART[smallClaw|GrapplingDevice] +{ + MODULE + { + name = ClawExtension + } +} diff --git a/BDArmory/Distribution/GameData/BDArmory/MMPatches/BDA_GroundRadar.cfg b/BDArmory/Distribution/GameData/BDArmory/MMPatches/BDA_GroundRadar.cfg deleted file mode 100644 index 2ff0431b8..000000000 --- a/BDArmory/Distribution/GameData/BDArmory/MMPatches/BDA_GroundRadar.cfg +++ /dev/null @@ -1,107 +0,0 @@ -+PART[bdRadome1snub] -{ - @name = bdRadome1snubGA - @title = APG-77v1 air-to-ground Radar (Snub) - @description = The AN/APG-77v1 is a forward facing, aerodynamically housed, solid-state, active electronically scanned array (AESA) radar. It provides full air-to-ground functionality at a MAX operating range of 40km against stationary and moving targets within a 120 degree field of view. This particular unit is optimized for Ground combat, and has difficulties locking air targets. - @MODULE[ModuleRadar] - { - @radarGroundClutterFactor = 1.7 - @radarName = APG-77 atg - @radarDetectionCurve - { - key = 0 0 0 0 - key = 5 0.9 0.29 0.31 - key = 10 3 0.48 0.51 - key = 15 5.9 0.62 0.69 - key = 20 10 0.96 0.9 - key = 25 14.1 0.71 0.71 - key = 30 17.3 0.58 0.58 - key = 35 20 0.48 0.61 - key = 40 35 9.18 1.5 - } - @radarLockTrackCurve - { - key = 0 0 0 0 - key = 5 0.9 0.47 0.44 - key = 10 3.5 0.59 0.59 - key = 15 7 0.73 0.71 - key = 20 11 0.79 0.9 - key = 25 16 1.05 1.05 - key = 30 21 1.09 0.9 - key = 35 25 0.48 0.49 - key = 40 40 9.18 1.5 - } - } -} - -+PART[bdRadome1inline] -{ - @name = bdRadome1inlineGA - @title = APG-77v1 air-to-ground Radar (Inline) - @description = The AN/APG-77v1 is a forward facing, aerodynamically housed, solid-state, active electronically scanned array (AESA) radar. It provides full air-to-ground functionality at a MAX operating range of 40km against stationary and moving targets within a 120 degree field of view. This particular unit is optimized for Ground combat, and has difficulties locking air targets. - @MODULE[ModuleRadar] - { - @radarGroundClutterFactor = 1.7 - @radarName = APG-77 atg - @radarDetectionCurve - { - key = 0 0 0 0 - key = 5 0.9 0.29 0.31 - key = 10 3 0.48 0.51 - key = 15 5.9 0.62 0.69 - key = 20 10 0.96 0.9 - key = 25 14.1 0.71 0.71 - key = 30 17.3 0.58 0.58 - key = 35 20 0.48 0.61 - key = 40 35 9.18 1.5 - } - @radarLockTrackCurve - { - key = 0 0 0 0 - key = 5 0.9 0.47 0.44 - key = 10 3.5 0.59 0.59 - key = 15 7 0.73 0.71 - key = 20 11 0.79 0.9 - key = 25 16 1.05 1.05 - key = 30 21 1.09 0.9 - key = 35 25 0.48 0.49 - key = 40 40 9.18 1.5 - } - } -} - -+PART[bdRadome1] -{ - @name = bdRadome1GA - @title = APG-77v1 air-to-ground Radar - @description = The AN/APG-77v1 is a forward facing, aerodynamically housed, solid-state, active electronically scanned array (AESA) radar. It provides full air-to-ground functionality at a MAX operating range of 40km against stationary and moving targets within a 120 degree field of view. This particular unit is optimized for Ground combat, and has difficulties locking air targets. - @MODULE[ModuleRadar] - { - @radarGroundClutterFactor = 1.7 - @radarName = APG-77 atg - @radarDetectionCurve - { - key = 0 0 0 0 - key = 5 0.9 0.29 0.31 - key = 10 3 0.48 0.51 - key = 15 5.9 0.62 0.69 - key = 20 10 0.96 0.9 - key = 25 14.1 0.71 0.71 - key = 30 17.3 0.58 0.58 - key = 35 20 0.48 0.61 - key = 40 35 9.18 1.5 - } - @radarLockTrackCurve - { - key = 0 0 0 0 - key = 5 0.9 0.47 0.44 - key = 10 3.5 0.59 0.59 - key = 15 7 0.73 0.71 - key = 20 11 0.79 0.9 - key = 25 16 1.05 1.05 - key = 30 21 1.09 0.9 - key = 35 25 0.48 0.49 - key = 40 40 9.18 1.5 - } - } -} diff --git a/BDArmory/Distribution/GameData/BDArmory/MMPatches/BDA_TweakScale.cfg b/BDArmory/Distribution/GameData/BDArmory/MMPatches/BDA_TweakScale.cfg index 62cc0dc51..201def21b 100644 --- a/BDArmory/Distribution/GameData/BDArmory/MMPatches/BDA_TweakScale.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/MMPatches/BDA_TweakScale.cfg @@ -1,4 +1,4 @@ -@PART[bdWarheadSmall] +@PART[bdWarheadSmall]:NEEDS[TweakScale] { %MODULE[TweakScale] { @@ -7,7 +7,7 @@ } } -@PART[awacsRadar] +@PART[awacsRadar]:NEEDS[TweakScale] { %MODULE[TweakScale] { @@ -23,4 +23,34 @@ name = TweakScale type = free_square } -} \ No newline at end of file +} + +@PART[BDAcUniversalAmmoBox]:NEEDS[TweakScale] +{ + MODULE + { + name = TweakScale + type = surface + minScale = 0.25 + maxScale = 4 + defaultScale = 1 + scaleFactors = 0.5, 1, 2, 4 + incrementSlide = 0.05, 0.1, 0.2 + scaleNames = Half, Full, Double, Quadruple + } +} + +@PART[UniversalAmmoBoxBDA]:NEEDS[TweakScale]//old legacy omni ammo box +{ + MODULE + { + name = TweakScale + type = surface + minScale = 0.25 + maxScale = 4 + defaultScale = 1 + scaleFactors = 0.5, 1, 2, 4 + incrementSlide = 0.05, 0.1, 0.2 + scaleNames = Half, Full, Double, Quadruple + } +} diff --git a/BDArmory/Distribution/GameData/BDArmory/MMPatches/BDA_battledamage.cfg b/BDArmory/Distribution/GameData/BDArmory/MMPatches/BDA_battledamage.cfg index 46ccc077e..a393cb106 100644 --- a/BDArmory/Distribution/GameData/BDArmory/MMPatches/BDA_battledamage.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/MMPatches/BDA_battledamage.cfg @@ -32,6 +32,12 @@ { } } +@PART[proceduralTankLiquid] +{ + %MODULE[ModuleSelfSealingTank] + { + } +} @PART[*]:HAS[@MODULE[ModuleEngines]] { @@ -51,6 +57,13 @@ { } } +@PART[*]:HAS[@MODULE[ModuleB9PartSwitch]] +{ + %MODULE[ModuleSelfSealingTank] + { + } +} + //Mod configs //AviatorArsenal @PART[7mmBox|50calBox|13mmBox|15mmBox|20mmBox|23mmBox|30mmBox|40mmBox|75mmBox] diff --git a/BDArmory/Distribution/GameData/BDArmory/MMPatches/StructPWing.cfg b/BDArmory/Distribution/GameData/BDArmory/MMPatches/StructPWing.cfg new file mode 100644 index 000000000..78aafbc81 --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/MMPatches/StructPWing.cfg @@ -0,0 +1,55 @@ ++PART[B9_Aero_Wing_Procedural_TypeA] +{ + @name = B9_Aero_Wing_Procedural_Panel + @title = B9-PW Procedural Structural Panel + @category = Structural + @description = Procedural Structural Panel you can shape in any way you want using the context menu. Press J while pointing at this part to open the editor window allowing you to edit the shape and materials of this part. You can exit the editing mode by switching to editing of another part in the very same way, or by pressing J again, or by closing the window. The window can also be opened and closed using the B9 button in the bottom-right corner of the screen. THIS PART WILL NOT GENERATE LIFT. + + @MODULE[ModuleLiftingSurface] + { + @deflectionLiftCoeff = 0 + } +} + +@PART[B9_Aero_Wing_Procedural_Panel]:NEEDS[ferramGraph]:FINAL +{ + !MODULE[ModuleLiftingSurface] {} + !MODULE[FARWingAerodynamicModel] {} +} + +@PART[B9_Aero_Wing_Procedural_Panel]:HAS[@MODULE[WingProcedural]]:FOR[B9_Aerospace_WingStuff]:NEEDS[TexturesUnlimited&!TURD/TU_B9_ProcWings] +{ + MODULE + { + name = SSTURecolorGUI + } + + MODULE + { + name = KSPTextureSwitch + transformName = surface + sectionName = Surface + + currentTextureSet = Smooth-Metal-Solid + textureSet = Smooth-Metal-Solid + } + + MODULE + { + name = KSPTextureSwitch + transformName = frame + sectionName = Frame + + currentTextureSet = Smooth-Metal-Solid + textureSet = Smooth-Metal-Solid + } + + MODULE + { + name = KSPTextureSwitch + sectionName = Edge + + currentTextureSet = B9PWings-edge-metal + textureSet = B9PWings-edge-metal + } +} diff --git a/BDArmory/Distribution/GameData/BDArmory/Models/explosion/nuke/Plume.png b/BDArmory/Distribution/GameData/BDArmory/Models/explosion/nuke/Plume.png index b6def4264..0524f5b87 100644 Binary files a/BDArmory/Distribution/GameData/BDArmory/Models/explosion/nuke/Plume.png and b/BDArmory/Distribution/GameData/BDArmory/Models/explosion/nuke/Plume.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Models/explosion/nuke/bigsmoke.png b/BDArmory/Distribution/GameData/BDArmory/Models/explosion/nuke/bigsmoke.png index e14256aca..fc8c43761 100644 Binary files a/BDArmory/Distribution/GameData/BDArmory/Models/explosion/nuke/bigsmoke.png and b/BDArmory/Distribution/GameData/BDArmory/Models/explosion/nuke/bigsmoke.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Models/explosion/nuke/nukeBlast.mu b/BDArmory/Distribution/GameData/BDArmory/Models/explosion/nuke/nukeBlast.mu index 99b7b621d..4aac63e5d 100644 Binary files a/BDArmory/Distribution/GameData/BDArmory/Models/explosion/nuke/nukeBlast.mu and b/BDArmory/Distribution/GameData/BDArmory/Models/explosion/nuke/nukeBlast.mu differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Models/explosion/nuke/nukeBoom.ogg b/BDArmory/Distribution/GameData/BDArmory/Models/explosion/nuke/nukeBoom.ogg new file mode 100644 index 000000000..efb55f73e Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Models/explosion/nuke/nukeBoom.ogg differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Models/explosion/nuke/nukeBoom.wav b/BDArmory/Distribution/GameData/BDArmory/Models/explosion/nuke/nukeBoom.wav deleted file mode 100644 index 810b46032..000000000 Binary files a/BDArmory/Distribution/GameData/BDArmory/Models/explosion/nuke/nukeBoom.wav and /dev/null differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Models/explosion/nuke/nukeFlash.mu b/BDArmory/Distribution/GameData/BDArmory/Models/explosion/nuke/nukeFlash.mu index ebd858e03..e677a610f 100644 Binary files a/BDArmory/Distribution/GameData/BDArmory/Models/explosion/nuke/nukeFlash.mu and b/BDArmory/Distribution/GameData/BDArmory/Models/explosion/nuke/nukeFlash.mu differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Models/explosion/nuke/nukePlume.mu b/BDArmory/Distribution/GameData/BDArmory/Models/explosion/nuke/nukePlume.mu index 54d3d2c51..287e70c7f 100644 Binary files a/BDArmory/Distribution/GameData/BDArmory/Models/explosion/nuke/nukePlume.mu and b/BDArmory/Distribution/GameData/BDArmory/Models/explosion/nuke/nukePlume.mu differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Models/explosion/nuke/nukeScatter.mu b/BDArmory/Distribution/GameData/BDArmory/Models/explosion/nuke/nukeScatter.mu index a4bcc5c57..d2717ce0d 100644 Binary files a/BDArmory/Distribution/GameData/BDArmory/Models/explosion/nuke/nukeScatter.mu and b/BDArmory/Distribution/GameData/BDArmory/Models/explosion/nuke/nukeScatter.mu differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Models/explosion/nuke/nukeShock.mu b/BDArmory/Distribution/GameData/BDArmory/Models/explosion/nuke/nukeShock.mu index 52a0c53b6..667dcc678 100644 Binary files a/BDArmory/Distribution/GameData/BDArmory/Models/explosion/nuke/nukeShock.mu and b/BDArmory/Distribution/GameData/BDArmory/Models/explosion/nuke/nukeShock.mu differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/20mmVulcan/vulcanTurret.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/20mmVulcan/vulcanTurret.cfg index b270b8b48..1563f2a85 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/20mmVulcan/vulcanTurret.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/20mmVulcan/vulcanTurret.cfg @@ -78,10 +78,10 @@ PART fireAnimName = fireAnimation spinDownAnimation = true - SpoolUpTime = 0.3 + SpoolUpTime = 0.15 roundsPerMinute = 5500 - maxDeviation = 0.175 + maxDeviation = 1.02 maxEffectiveDistance = 2500 maxTargetingRange = 5000 diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/ABL/ABL.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/ABL/ABL.cfg index b025e446f..044fec95e 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/ABL/ABL.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/ABL/ABL.cfg @@ -85,6 +85,10 @@ MODULE laserDamage = 1600 tanAngle = 0.0001 //controls how quickly damage scales down with distance + isAPS = true + APSType = missile + dualModeAPS = true + projectileColor = 255, 20, 0, 128 //RGBA 0-255 tracerStartWidth = 0.3 tracerEndWidth = 0.3 @@ -95,7 +99,9 @@ MODULE fireSoundPath = BDArmory/Parts/ABL/sounds/laser chargeSoundPath = BDArmory/Parts/ABL/sounds/charge - overheatSoundPath = BDArmory/Parts/50CalTurret/sounds/turretOverheat + overheatSoundPath = BDArmory/Parts/ABL/sounds/drain + oneShotSound = false + soundRepeatTime = 0 } } diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/ABL/sounds/charge.ogg b/BDArmory/Distribution/GameData/BDArmory/Parts/ABL/sounds/charge.ogg index 1c5cfc204..a7f9ecb19 100644 Binary files a/BDArmory/Distribution/GameData/BDArmory/Parts/ABL/sounds/charge.ogg and b/BDArmory/Distribution/GameData/BDArmory/Parts/ABL/sounds/charge.ogg differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/ABL/sounds/charge.ogg.orig b/BDArmory/Distribution/GameData/BDArmory/Parts/ABL/sounds/charge.ogg.orig new file mode 100644 index 000000000..1c5cfc204 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Parts/ABL/sounds/charge.ogg.orig differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/ABL/sounds/drain.ogg b/BDArmory/Distribution/GameData/BDArmory/Parts/ABL/sounds/drain.ogg new file mode 100644 index 000000000..867ad25ca Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Parts/ABL/sounds/drain.ogg differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/AGM86-17/AGM86-17.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/AGM86-17/AGM86-17.cfg index a730ad379..6aa697545 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/AGM86-17/AGM86-17.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/AGM86-17/AGM86-17.cfg @@ -108,6 +108,7 @@ MODULE { name = BDExplosivePart tntMass = 1300 + caliber = 620 warheadType = ShapedCharge } diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/AIR-2/part.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/AIR-2/part.cfg index 8b916c1f3..0d4cde34b 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/AIR-2/part.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/AIR-2/part.cfg @@ -10,7 +10,7 @@ PART author = SuicidalInsanity // --- asset parameters --- - mesh = model.mu + mesh = Model.mu rescaleFactor = 1 @@ -27,7 +27,7 @@ PART subcategory = 0 bulkheadProfiles = srf title = #loc_BDArmory_part_genie_title //AIR-2 Genie Air-To-Air Rocket - manufacturer = loc_BDArmory_agent_title + manufacturer = #loc_BDArmory_agent_title description = #loc_BDArmory_part_genie_description //1.5kt Nuclear Anti-Air Rocket. // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision attachRules = 1,1,0,0,1 diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/AmmoBox/BDAcUniversalAmmoBox.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/AmmoBox/BDAcUniversalAmmoBox.cfg index b6d044c25..b90cc2002 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/AmmoBox/BDAcUniversalAmmoBox.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/AmmoBox/BDAcUniversalAmmoBox.cfg @@ -54,25 +54,14 @@ maxTemp = 3600 } MODULE { - name = ModuleAmmoSwitch//47 - resourceNames = Empty; 7.62x39Ammo; 7.7x56Ammo; 7.92x57mmMauser; 9x19mmParaAmmo; 50CalAmmo; 20x21Ammo; 20x102Ammo; 20x163Ammo; 23x115Ammo; 23x152Ammo; 25x137Ammo; 30x165Ammo; 30x173Ammo; 30x173HEAmmo; 37mmFlaKAmmo; 40x53Ammo; 40x53HeAmmo; 40x311Ammo; 54cmMortarShells; 57x438Ammo; TungstenShell; 75x714Ammo; 76x636Ammo; 3inchShells; 90mmShells; 100mmShells; 4p5inchQFShells; 105mmShells; 105mmHEShells; 120mmAmmo; 122mmQFShells; 130Shells; 5/62Shell; 138_140Shells; 152Shells; 155Shells; 180Shells; 203Shells; 12inShells; 356Shells; 356ApAmmo; 380Shells; M65ShellAmmo; 406mmNuclearShells; 16inchShells; 460Shells - resourceAmounts = 0; 500; 500; 500; 500; 400; 400; 400; 350; 350; 350; 350; 300; 300; 300; 300; 300; 300; 300; 200; 200; 200; 40; 40; 40; 30; 30; 30; 30; 30; 25; 25; 25; 20; 15; 15; 15; 15; 10; 8; 8; 8; 6; 4; 4; 4; 4 //47 + name = ModuleAmmoSwitch//48 + resourceNames = Empty; 7.62x39Ammo; 7.7x56Ammo; 7.92x57mmMauser; 9x19mmParaAmmo; 50CalAmmo; 20x21Ammo; 20x102Ammo; 20x163Ammo; 23x115Ammo; 23x152Ammo; 25x137Ammo; 30x165Ammo; 30x173Ammo; 30x173HEAmmo; 37mmFlaKAmmo; 40x53Ammo; 40x53HeAmmo; 40x311Ammo; 54cmMortarShells; 57x438Ammo; TungstenShell; 75x714Ammo; 76x636Ammo; 3inchShells; 90mmShells; 100mmShells; 4p5inchQFShells; 105mmShells; 105mmHEShells; 120mmAmmo; 122mmQFShells; 130Shells; 5/62Shell; 138_140Shells; 152Shells; 155Shells; 180Shells; 203Shells; 12inShells; 356Shells; 356ApAmmo; 380Shells; M65ShellAmmo; 406mmNuclearShells; 16inchShells; 460Shells; Rockets + resourceAmounts = 0; 500; 500; 500; 500; 400; 400; 400; 350; 350; 350; 350; 300; 300; 300; 300; 300; 300; 300; 200; 200; 200; 40; 40; 40; 30; 30; 30; 30; 30; 25; 25; 25; 20; 15; 15; 15; 15; 10; 8; 8; 8; 6; 4; 4; 4; 4; 40 //48 basePartMass = 0.1 showInfo = true displayCurrentTankCost = true } - MODULE - { - name = TweakScale - type = surface - minScale = 0.25 - maxScale = 4 - defaultScale = 1 - scaleFactors = 0.5, 1, 2, 4 - incrementSlide = 0.05, 0.1, 0.2 - scaleNames = Half, Full, Double, Quadruple - } - MODEL + MODEL { model = BDArmory/Parts/AmmoBox/model texture = texture, BDArmory/Parts/AmmoBox/textureUni diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/AmmoBox/UniversalAmmoBoxBDA.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/AmmoBox/UniversalAmmoBoxBDA.cfg index 38835c67d..b4157f913 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/AmmoBox/UniversalAmmoBoxBDA.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/AmmoBox/UniversalAmmoBoxBDA.cfg @@ -54,25 +54,14 @@ maxTemp = 3600 } MODULE { - name = FSfuelSwitch//47 - resourceNames = Empty; 7.62x39Ammo; 7.7x56Ammo; 7.92x57mmMauser; 9x19mmParaAmmo; 50CalAmmo; 20x21Ammo; 20x102Ammo; 20x163Ammo; 23x115Ammo; 23x152Ammo; 25x137Ammo; 30x165Ammo; 30x173Ammo; 30x173HEAmmo; 37mmFlaKAmmo; 40x53Ammo; 40x53HeAmmo; 40x311Ammo; 54cmMortarShells; 57x438Ammo; TungstenShell; 75x714Ammo; 76x636Ammo; 3inchShells; 90mmShells; 100mmShells; 4p5inchQFShells; 105mmShells; 105mmHEShells; 120mmAmmo; 122mmQFShells; 130Shells; 5/62Shell; 138_140Shells; 152Shells; 155Shells; 180Shells; 203Shells; 12inShells; 356Shells; 356ApAmmo; 380Shells; M65ShellAmmo; 406mmNuclearShells; 16inchShells; 460Shells - resourceAmounts = 0; 500; 500; 500; 500; 400; 400; 400; 350; 350; 350; 350; 300; 300; 300; 300; 300; 300; 300; 200; 200; 200; 40; 40; 40; 30; 30; 30; 30; 30; 25; 25; 25; 20; 15; 15; 15; 15; 10; 8; 8; 8; 6; 4; 4; 4; 4 //47 + name = FSfuelSwitch//48 + resourceNames = Empty; 7.62x39Ammo; 7.7x56Ammo; 7.92x57mmMauser; 9x19mmParaAmmo; 50CalAmmo; 20x21Ammo; 20x102Ammo; 20x163Ammo; 23x115Ammo; 23x152Ammo; 25x137Ammo; 30x165Ammo; 30x173Ammo; 30x173HEAmmo; 37mmFlaKAmmo; 40x53Ammo; 40x53HeAmmo; 40x311Ammo; 54cmMortarShells; 57x438Ammo; TungstenShell; 75x714Ammo; 76x636Ammo; 3inchShells; 90mmShells; 100mmShells; 4p5inchQFShells; 105mmShells; 105mmHEShells; 120mmAmmo; 122mmQFShells; 130Shells; 5/62Shell; 138_140Shells; 152Shells; 155Shells; 180Shells; 203Shells; 12inShells; 356Shells; 356ApAmmo; 380Shells; M65ShellAmmo; 406mmNuclearShells; 16inchShells; 460Shells; Rockets + resourceAmounts = 0; 500; 500; 500; 500; 400; 400; 400; 350; 350; 350; 350; 300; 300; 300; 300; 300; 300; 300; 200; 200; 200; 40; 40; 40; 30; 30; 30; 30; 30; 25; 25; 25; 20; 15; 15; 15; 15; 10; 8; 8; 8; 6; 4; 4; 4; 4; 40 //48 basePartMass = 0.1 showInfo = true displayCurrentTankCost = true } - MODULE - { - name = TweakScale - type = surface - minScale = 0.25 - maxScale = 4 - defaultScale = 1 - scaleFactors = 0.5, 1, 2, 4 - incrementSlide = 0.05, 0.1, 0.2 - scaleNames = Half, Full, Double, Quadruple - } - MODEL + MODEL { model = BDArmory/Parts/AmmoBox/model texture = texture, BDArmory/Parts/AmmoBox/textureUni diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/IsoTriPanel.mu b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/IsoTriPanel.mu new file mode 100644 index 000000000..01b7bb0e3 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/IsoTriPanel.mu differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/IsoTripanelArmor.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/IsoTripanelArmor.cfg new file mode 100644 index 000000000..51140aac2 --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/IsoTripanelArmor.cfg @@ -0,0 +1,95 @@ +PART +{ +name = BD_PanelArmorIsoTri +module = Part +author = SuicidalInsanity +buoyancy = 1 + MODEL + { + model = BDArmory/Parts/ArmorPlate/IsoTriPanel + scale = 1.0, 1.0, 1.0 + } +rescaleFactor = 1 +node_attach = 0.0, 0.0, 0.5, 0.0, 0.0, -1, 0 +node_stack_right = 0.0, 0.0, 0.5, 0.0, 0.0, 1, 0 +node_stack_top = 0.25, 0.0, 0.0, 1, 0, -0.5, 0 +node_stack_bottom = -0.25, 0.0, 0.0, -1, 0, -0.5, 0 + +TechRequired = composites +entryCost = 7200 +cost = 5 +category = Structural +bdacategory = Armor +subcategory = 0 +bulkheadProfiles = srf +title = #loc_BDArmory_part_TriIsoPanel_title //BD Armor Panel Oblique Triangle +manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics +description = #loc_BDArmory_part_Tripanel_description //A sturdy Universal Structural Panel that can be configured to be a variety of sizes and use a variety of materials, perfect for constructing or armoring all sorts of things. +attachRules = 1,1,1,1,1 + +// --- standard part parameters --- +mass = 0.005 +dragModelType = default +maximum_drag = 0.2 +minimum_drag = 0.2 +angularDrag = 1 +crashTolerance = 80 +breakingForce = 200 +breakingTorque = 200 +maxTemp = 2000 +fuelCrossFeed = True +tags = armor Armo Ship Afv panel + + MODULE + { + name = HitpointTracker + ArmorThickness = 25 + maxSupportedArmor = 500 + armorVolume = 0.5 + isTriangularPanel = true + } + MODULE + { + name = BDAdjustableArmor + ArmorTransformName = ArmorTransform + isTriangularPanel = true + TriangleType = Scalene + stackNodePosition = bottom,-0.25,0,0.0;top,0.25,0.0,0.0;right,0.0,0.0,0.5 + } + MODULE + { + name = ModuleMirrorPlacement + applyMirrorRotationXAxis = false + } + + MODULE + { + name = ModulePartVariants + primaryColor = #4F5352 + baseDisplayName = Dark Gray Steel + VARIANT + { + name = Light Gray + displayName = Light Gray Steel + primaryColor = #808080 + TEXTURE + { + mainTextureURL = ArmorPlate/armorpanelNRM + } + } + VARIANT + { + name = CarrierDeck + displayName = Carrier Deck + primaryColor = #282828 + secondaryColor = #333333 + TEXTURE + { + shader = KSP/Bumped + mainTextureURL = ArmorPlate/CarrierDeck + _BumpMap = ArmorPlate/CarrierDeckNRM + } + } + } + +} \ No newline at end of file diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD0.5x0.5panelArmor.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD0.5x0.5panelArmor.cfg index fd70febe2..407378e60 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD0.5x0.5panelArmor.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/Legacy/BD0.5x0.5panelArmor.cfg @@ -46,7 +46,6 @@ TechRequired = Unresearcheable entryCost = 7200 cost = 5 category = none -bdacategory = Armor subcategory = 0 bulkheadProfiles = srf title = BD 0.5x0.5 panel Armor diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/TripanelArmor.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/TripanelArmor.cfg index 7ba773d19..6208fb355 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/TripanelArmor.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/TripanelArmor.cfg @@ -9,22 +9,11 @@ buoyancy = 1 model = BDArmory/Parts/ArmorPlate/TriPanel scale = 1.0, 1.0, 1.0 } - NODE - { - name = Node1 - transform = N1 - size = 0 - method = FIXED_JOINT - } - NODE - { - name = Node2 - transform = N2 - size = 0 - method = FIXED_JOINT - } rescaleFactor = 1 -node_attach = -0.0, 0, 0.0, 0, 0, 1, 0 +node_attach = 0.0, 0.0, 0.5, 0.0, 0.0, -1, 0 +node_stack_bottom = -0.5, 0.0, 0.0, -1.0, 0.0, 0, 0 +node_stack_right = 0.0, 0.0, 0.5, 0.0, 0.0, 1, 0 +node_stack_side = 0.0, 0.0, 0.0, 1.0, 0.0,-1, 0 TechRequired = composites entryCost = 7200 @@ -33,7 +22,7 @@ category = Structural bdacategory = Armor subcategory = 0 bulkheadProfiles = srf -title = #loc_BDArmory_part_TriPanel_title //BD Armor Panel Triangle +title = #loc_BDArmory_part_TriPanel_title //BD Armor Panel Right Triangle manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics description = #loc_BDArmory_part_Tripanel_description //A sturdy Universal Structural Panel that can be configured to be a variety of sizes and use a variety of materials, perfect for constructing or armoring all sorts of things. attachRules = 1,1,1,1,1 @@ -63,9 +52,9 @@ tags = armor Armo Ship Afv panel { name = BDAdjustableArmor ArmorTransformName = ArmorTransform - Node1Name = N1 - Node2Name = N2 - isTriangularPanel = True + isTriangularPanel = true + TriangleType = Right + stackNodePosition = bottom,-0.5,0.0,0.0;right,0.0,0.0,0.5;side,0.0,0.0,0.0 } MODULE { diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/panelArmor.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/panelArmor.cfg index 1a03a2bc4..2ff1883ef 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/panelArmor.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/ArmorPlate/panelArmor.cfg @@ -9,36 +9,12 @@ buoyancy = 1 model = BDArmory/Parts/ArmorPlate/Panel scale = 1.0, 1.0, 1.0 } - NODE - { - name = Node1 - transform = N1 - size = 0 - method = FIXED_JOINT - } - NODE - { - name = Node2 - transform = N2 - size = 0 - method = FIXED_JOINT - } - NODE - { - name = Node3 - transform = N3 - size = 0 - method = FIXED_JOINT - } - NODE - { - name = Node4 - transform = N4 - size = 0 - method = FIXED_JOINT - } rescaleFactor = 1 -node_attach = 0.0, 0.0, -0.5, 0, 0, 1, 0 +node_attach = 0.0, 0.0, -0.5, 0.0, 0.0, 1, 0 +node_stack_right = 0.0, 0.0, 0.5, 0.0, 0.0, 1, 0 +node_stack_top = 0.5, 0.0, 0.0, 1.0, 0.0, 0, 0 +node_stack_left = 0.0, 0.0, -0.5, 0.0, 0.0, -1, 0 +node_stack_bottom = -0.5, 0.0, 0.0, -1.0, 0.0, 0, 0 TechRequired = composites entryCost = 7200 @@ -76,10 +52,8 @@ tags = armor Armo Ship Afv panel { name = BDAdjustableArmor ArmorTransformName = ArmorTransform - Node1Name = N1 - Node2Name = N2 - Node3Name = N3 - Node4Name = N4 + stackNodePosition = right,0.0,0.0,0.5;top, 0.5,0,0;left,0,0,-0.5;bottom,-0.5,0,0 + maxScale = 16 } MODULE diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/EJ200/caesar_hone_60.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/EJ200/caesar_hone_60.cfg index f1ff9cc3a..fd7a0bf45 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/EJ200/caesar_hone_60.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/EJ200/caesar_hone_60.cfg @@ -625,10 +625,4 @@ PART prestige = Exceptional } } - MODULE - { - name = TweakScale - type = stack - defaultScale = 0.625 - } } diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/EJ200/caesar_hone_60_TS.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/EJ200/caesar_hone_60_TS.cfg new file mode 100644 index 000000000..c874e7d9f --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/EJ200/caesar_hone_60_TS.cfg @@ -0,0 +1,9 @@ +@PART[BDA_EJ200]:NEEDS[TweakScale] //Thanks, Stardust! +{ + #@TWEAKSCALEBEHAVIOR[Engine]/MODULE[TweakScale] { } + %MODULE[TweakScale] + { + %type = stack + %defaultScale = 0.625 + } +} \ No newline at end of file diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/Gau-22A/part.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/Gau-22A/part.cfg index 9f36a8193..4546f3631 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/Gau-22A/part.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/Gau-22A/part.cfg @@ -47,16 +47,16 @@ MODULE name = ModuleWeapon fireTransformName = fireTransform - + shortName = GAU-22 hasDeployAnim = true deployAnimName = GauUnpack hasFireAnimation = true fireAnimName = GauFireAnim spinDownAnimation = true - SpoolUpTime = 0.3 + SpoolUpTime = 0.2 roundsPerMinute = 3300 - maxDeviation = 0.25 + maxDeviation = 0.6405 maxEffectiveDistance = 3000 maxTargetingRange = 5000 @@ -78,7 +78,7 @@ MODULE //oneShotWorldParticles = true maxHeat = 3600 - heatPerShot = 38 + heatPerShot = 48 heatLoss = 820 autoProxyTrackRange = 1200 diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/GoalKeeperBDAcMk1/GoalKeeperBDAcMk1.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/GoalKeeperBDAcMk1/GoalKeeperBDAcMk1.cfg index 6cd5dd10c..cd7ec13a1 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/GoalKeeperBDAcMk1/GoalKeeperBDAcMk1.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/GoalKeeperBDAcMk1/GoalKeeperBDAcMk1.cfg @@ -85,7 +85,7 @@ PART spinDownAnimation = true SpoolUpTime = 0.3 roundsPerMinute = 4200 - maxDeviation = 0.50 + maxDeviation = 0.675 maxEffectiveDistance = 4000 maxTargetingRange = 5000 @@ -98,6 +98,9 @@ PART bulletDrop = true weaponType = ballistic + isAPS = true + APSType = missile + dualModeAPS = true tracerLength = 0 tracerDeltaFactor = 2.75 diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/GoalKeeperBDAcMk2/BDAcGKmk2.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/GoalKeeperBDAcMk2/BDAcGKmk2.cfg index c559c73f7..0d8cad5f1 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/GoalKeeperBDAcMk2/BDAcGKmk2.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/GoalKeeperBDAcMk2/BDAcGKmk2.cfg @@ -33,7 +33,7 @@ subcategory = 0 bulkheadProfiles = srf title = #loc_BDArmory_part_BDAcGKmk2_title //Goalkeeper MK2 CIWS manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics -description = #loc_BDArmory_part_BDAcGKmk2_description //A 7 barrel 30mm rotary cannon with full swivel range. This MK 2 version was found covered in overspray and paint cans around the back of the hangar at the old KSC, developed from the MK1 to reduce the incidence of hearing loss amongst early target pointers. This MK2 has some slight advantages over the MK1, equipped with Infra red targeting and Radar data reciever The 30x173mm high explosive rounds are only a slight improvement over the MK1 ammunition in that they at least take slightly longer to lose interest in flying and so have a good chance of reaching the target, but this weapon was never equipped to feature automatic fuse timing. +description = #loc_BDArmory_part_BDAcGKmk2_description //A 7 barrel 30mm rotary cannon with full swivel range. This MK 2 version was found covered in overspray and paint cans around the back of the hangar at the old KSC, developed from the MK1 to reduce the incidence of hearing loss amongst early target pointers. This MK2 has some slight advantages over the MK1, equipped with Infra red targeting and Radar data receiver The 30x173mm high explosive rounds are only a slight improvement over the MK1 ammunition in that they at least take slightly longer to lose interest in flying and so have a good chance of reaching the target, but this weapon was never equipped to feature automatic fuse timing. // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision attachRules = 1,1,0,0,1 @@ -87,7 +87,7 @@ MODULE spinDownAnimation = true SpoolUpTime = 0.3 roundsPerMinute = 4200 - maxDeviation = 0.40 + maxDeviation = 0.64 maxEffectiveDistance = 4000 maxTargetingRange = 5000 @@ -100,6 +100,9 @@ MODULE bulletDrop = true weaponType = ballistic + isAPS = true + APSType = missile + dualModeAPS = true tracerLength = 0 tracerDeltaFactor = 2.75 @@ -161,7 +164,7 @@ MODULE canScan = false // scanning/detecting targets (volume search) canLock = false // locking/tracking targets (fire control) canTrackWhileScan = false // continue scanning while tracking a locked target - canRecieveRadarData = true // can work as passive data receiver (NOTE THE SPELLING! [SIC]) + canReceiveRadarData = true // can work as passive data receiver minSignalThreshold = 350 // DEPRECATED, NO LONGER USED! use detection float curve! minLockedSignalThreshold = 120 // DEPRECATED, NO LONGER USED! use locktrack float curve! diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/GravityGun/part.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/GravityGun/part.cfg index dd3b00332..f17a76565 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/GravityGun/part.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/GravityGun/part.cfg @@ -61,18 +61,19 @@ stagingIcon = SOLID_BOOSTER weaponType = laser pulseLaser = true //change this to false if beamlasers preferred - impulseWeapon = true //does it impart impulse instead of damage? - Impulse = 5000 //impulse per hit. Per sec if pulselaser = false + laserDamage = 0 //set to > 0 if you want impluse/gravitic lasers to also do damage per hit + impulseWeapon = true //does it impart impulse + Impulse = 50 //impulse (kN) per hit. Per sec if pulselaser = false graviticWeapon = true // cause mass change in hit part? Independant of impulseWeapon - massAdjustment = 0.5 // mass change, in tons + massAdjustment = 0.25 // mass change, in tons ammoName = ElectricCharge requestResourceAmount = 1 projectileColor = 56, 83, 255, 240//RGBA 0-255 startColor = 56, 83, 255, 240 fadeColor = false - tracerStartWidth = 0.3 - tracerEndWidth = 0.3 + tracerStartWidth = 0.4 + tracerEndWidth = 0.4 tracerLength = 0 autoProxyTrackRange = 1200 diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/IRSTPod/irst.png b/BDArmory/Distribution/GameData/BDArmory/Parts/IRSTPod/irst.png new file mode 100644 index 000000000..fc67bc100 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Parts/IRSTPod/irst.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/IRSTPod/model.mu b/BDArmory/Distribution/GameData/BDArmory/Parts/IRSTPod/model.mu new file mode 100644 index 000000000..091d5731d Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Parts/IRSTPod/model.mu differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/IRSTPod/part.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/IRSTPod/part.cfg new file mode 100644 index 000000000..2103db3bb --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/IRSTPod/part.cfg @@ -0,0 +1,98 @@ +PART +{ + name = bahaIRSTpod + module = Part + author = SuicidalInsanity + rescaleFactor = 0.85 + node_stack_top = 0.0, 0.0, -0.28, 0, 0, -1, 1 + node_attach = 0.0, 0.28, -0.1082, 0.0, 0.0, 1 + + TechRequired = precisionEngineering + entryCost = 5500 + cost = 2000 + category = none + bdacategory = Radars + subcategory = 0 + bulkheadProfiles = size1 + title = #loc_BDArmory_part_bahaIRSTPod_title //AN/AAQ-42 IRST Pod + manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics + description = #loc_BDArmory_part_bahaIRSTPod_description //A forward facing InfraRed Search and Track system housed in an aerodynamic pod. It can scan and detect thermal signatures within a 120 degree field of view. It is optimized for air-to-air use, and has difficulties detecting surface targets. + + attachRules = 1,1,1,1,0 + stackSymmetry = 2 + mass = 0.2 + dragModelType = default + maximum_drag = 0.2 + minimum_drag = 0.2 + angularDrag = 2 + crashTolerance = 7 + maxTemp = 2000 + + MODULE + { + name = ModuleIRST + + // -- Section: General Configuration -- + IRSTName = AN/AAQ-42 IRST // if left empty part.title is used, but advised to set this to a nice printable text + //rotationTransformName = scanRotation //transform name for any animated irst elements that rotate while unit is active + //turretID = 0 // if needed + resourceDrain = 0.825 //EC/sec + // -- Section: Capabilities -- + irstRanging = false // true: IRST can provide target range similar to radar + omnidirectional = false // false: boresight scan irst + directionalFieldOfView = 120 // for omni and boresight + //boresightFOV = 10 // for boresight only + //scanRotationSpeed = 240 // degress per second + showDirectionWhileScan = false // can show target direction on radar screen. False: returns displayed as block only (no direction) + canScan = true // scanning/detecting targets (volume search) + + GroundClutterFactor = 0.16 // how much is the irst efficiency reduced to by ground clutter/look-down? + // 0.0 = reduced to 0% (=IMPOSSIBLE to detect ground targets) + // 1.0 = fully efficient (no difference between air & ground targets) + // default if unset: 0.16 + // Ground targets are going to be harder to pick out against background ground temperature. Doesn't apply to sea targets. + // values >1.0 are possible, meaning the irst is MORE efficient during look down than vs air targets. + + TempSensitivityCurve + { + // floatcurve that applies a tempModifier to the default KSP temperature values to make certain temperatures/IR spectrum more detectable + // This is applied to the target craft's heat values first, before the DetectionCurve and atmAttenuationCurve are evaluated + // For example, key = 350 2, will result in a part having 350 heat being evaluated for detection as if it has 350*2 = 700 heat + // This curve represents an example IRST optimized for MWIR (3-5μm, 578-963K), with standard SWIR (1.4-3μm, 963-2064K), and no LWIR (8-14μm, 206-361K) capability + // key = temp tempModifier + // key = 0 0 0 0 + // key = 361 0 0 0 // End of LWIR (8μm) + // key = 578 2 0 0 // Start of MWIR (5μm) + // key = 963 2 0 0 // End of MWIR/Start of SWIR (3μm) + // key = 1156 1 0 0 // 2.5μm + // key = 2064 1 0 0 // End of SWIR (1.4μm) + // key = temp tempModifier + key = 0 1 // Use this to ignore temperature sensitivity effects, this is the default if TempSensitivityCurve is not set + } + + DetectionCurve + { + // floatcurve to define at what range (km) which minimum thermal signature(k) can be detected. + // this defines both min/max range of the irst, and sensitivity/efficiency + // it is recommended to define an "assured detection range", at which all craft are detected regardless + // of their heatSig. This is achieved by using a min temp value of zero, thus detecting everything. + // key = distance temp + key = 0.0 300 //Ambient craft temp at Kerbin ASL is ~307k + key = 5 300 + key = 20 500 + key = 30 1000 + key = 45 3000 //max detection of 3000 heat at 45km + } + + atmAttenuationCurve + { + // floatcurve to define how IRST range is affected by local atmosphere density and temperature; thinner and colder air attenuates heat signatures less, increasing range + // key = atmdensity rangeModifier + key = 1 1 + key = 0.9 1.125 + key = 0.5 3 + key = 0.1 8 + key = 0 10 //vacuum, range set by sensor resolution + } + } +} diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/MissileBay/BombBay.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/MissileBay/BombBay.cfg index 9bf581b95..450718515 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/MissileBay/BombBay.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/MissileBay/BombBay.cfg @@ -30,7 +30,7 @@ PART } //Attachnode transforms require a 'rail_' prefix - + node_stack_top = 0.0, 0.286, 0, 0, 1, 0, 0 TechRequired = precisionEngineering entryCost = 1000 cost = 500 diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/Mk1OpenCockpit/ejector_seat.png b/BDArmory/Distribution/GameData/BDArmory/Parts/Mk1OpenCockpit/ejector_seat.png new file mode 100644 index 000000000..da80acdec Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Parts/Mk1OpenCockpit/ejector_seat.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/Mk1OpenCockpit/ejector_seat_Nrm_NRM.png b/BDArmory/Distribution/GameData/BDArmory/Parts/Mk1OpenCockpit/ejector_seat_Nrm_NRM.png new file mode 100644 index 000000000..5b9d95628 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Parts/Mk1OpenCockpit/ejector_seat_Nrm_NRM.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/Mk1OpenCockpit/model.mu b/BDArmory/Distribution/GameData/BDArmory/Parts/Mk1OpenCockpit/model.mu new file mode 100644 index 000000000..1c9db94cf Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Parts/Mk1OpenCockpit/model.mu differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/Mk1OpenCockpit/weaponizedcommandseat.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/Mk1OpenCockpit/weaponizedcommandseat.cfg index 8dc2aea71..f4ab00601 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/Mk1OpenCockpit/weaponizedcommandseat.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/Mk1OpenCockpit/weaponizedcommandseat.cfg @@ -2,25 +2,25 @@ PART { name = seatExternalCmdweaponized module = Part - author = CeruleanEyes + author = Eclipse, CeruleanEyes MODEL { - model = Squad/Parts/Command/externalCommandSeat/model + model = BDArmory/Parts/Mk1OpenCockpit/model } rescaleFactor = 1 - node_stack_top = 0.0, 0.0, 0.235, 0.0, 0.0, 1.0, 0, 1 - node_attach = 0.0, 0.0, 0.2, 0.0, 0.0, -1.0, 1, 1 + node_stack_top = 0.0, 0.0, 0.146, 0.0, 0.0, 1.0, 0, 1 + node_attach = 0.0, 0.0, 0.146, 0.0, 0.0, -1.0, 1, 1 // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision - attachRules = 1,1,0,0,1 + attachRules = 1,1,1,0,1 TechRequired = fieldScience entryCost = 8100 cost = 0 category = Pods subcategory = 0 - title = EAS-2 External Combat Seat - manufacturer = CeruleanEyes - description = The EAS-2 External Combat Seat Plus Pilot AI and Weapon Manager. + title = #loc_BDArmory_part_combatSeat_title //EAS-2 External Combat Seat + manufacturer = #loc_BDArmory_part_manufacturer + description = #loc_BDArmory_part_combatSeat_description //The EAS-2 External Combat Seat Plus Pilot AI and Weapon Manager. mass = 0.05 dragModelType = default maximum_drag = 0.05 diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/RBS-15-17/rbs15-17.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/RBS-15-17/rbs15-17.cfg index 3959a2ebd..bb0a535f2 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/RBS-15-17/rbs15-17.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/RBS-15-17/rbs15-17.cfg @@ -10,7 +10,7 @@ PART author = BahamutoD // --- asset parameters --- - mesh = model.mu + mesh = RBS-15-17.mu rescaleFactor = 1 @@ -103,7 +103,8 @@ PART MODULE { name = BDExplosivePart - tntMass = 300 + tntMass = 200 + caliber = 500 warheadType = ShapedCharge } diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/RBS-15-17AL/rbs15-17AL.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/RBS-15-17AL/rbs15-17AL.cfg index 4bc7eeb66..6bda54661 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/RBS-15-17AL/rbs15-17AL.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/RBS-15-17AL/rbs15-17AL.cfg @@ -10,7 +10,7 @@ PART author = BahamutoD // --- asset parameters --- - mesh = model.mu + mesh = RBS-15-17.mu rescaleFactor = 1 @@ -97,7 +97,8 @@ PART MODULE { name = BDExplosivePart - tntMass = 300 + tntMass = 200 + caliber = 500 warheadType = ShapedCharge } diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/ReactiveArmor/ReactiveArmor.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/ReactiveArmor/ReactiveArmor.cfg index 2062a311c..4b54a8812 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/ReactiveArmor/ReactiveArmor.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/ReactiveArmor/ReactiveArmor.cfg @@ -15,7 +15,7 @@ subcategory = 0 bulkheadProfiles = srf title = #loc_BDArmory_part_REA_title //BD 1x0.5 Reactive Armor manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics -description = #loc_BDArmory_part_Panel_description //A 1x0.5m section of Reactive Armor sections. Great for adding that little extra bit of protection on top of existing armor. +description = #loc_BDArmory_part_REA_Panel_description //A 1x0.5m section of Reactive Armor sections. Great for adding that little extra bit of protection on top of existing armor. attachRules = 1,1,1,1,1 // --- standard part parameters --- diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/Sidam/Model.mu b/BDArmory/Distribution/GameData/BDArmory/Parts/Sidam/Model.mu index 49655aaab..2748a0c67 100644 Binary files a/BDArmory/Distribution/GameData/BDArmory/Parts/Sidam/Model.mu and b/BDArmory/Distribution/GameData/BDArmory/Parts/Sidam/Model.mu differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/Sidam/SIDAM.png b/BDArmory/Distribution/GameData/BDArmory/Parts/Sidam/SIDAM.png index 46e2a4e61..faad4fe22 100644 Binary files a/BDArmory/Distribution/GameData/BDArmory/Parts/Sidam/SIDAM.png and b/BDArmory/Distribution/GameData/BDArmory/Parts/Sidam/SIDAM.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/Sidam/SIDAM_White.png b/BDArmory/Distribution/GameData/BDArmory/Parts/Sidam/SIDAM_White.png new file mode 100644 index 000000000..74ebf5a47 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Parts/Sidam/SIDAM_White.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/Sidam/part.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/Sidam/part.cfg index 9121c5799..ff02a716e 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/Sidam/part.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/Sidam/part.cfg @@ -75,7 +75,7 @@ MODULE spinDownAnimation = false roundsPerMinute = 1220 - maxDeviation = 0.12 + maxDeviation = 0.42 maxEffectiveDistance = 2500 maxTargetingRange = 5000 @@ -95,7 +95,7 @@ MODULE tracerDeltaFactor = 2.75 maxHeat = 3600 - heatPerShot = 50 + heatPerShot = 65 heatLoss = 750 fireSoundPath = BDArmory/Parts/Sidam/shot @@ -103,9 +103,43 @@ MODULE oneShotSound = true //explosion - explModelPath = BDArmory/Models/explosion/30mmExplosion - explSoundPath = BDArmory/Sounds/subExplode + explModelPath = BDArmory/Models/explosion/30mmExplosion + explSoundPath = BDArmory/Sounds/subExplode + + } +MODULE + { + name = ModulePartVariants + baseVariant = Default + useMultipleDragCubes = false + VARIANT + { + name = Default + displayName = #autoLOC_8007122 + themeName = Default + primaryColor = #ffffff + secondaryColor = #000000 + TEXTURE + { + mainTextureURL = BDArmory/Parts/Sidam/SIDAM + shader = KSP/Diffuse + } + } + VARIANT + { + name = White + displayName = #autoLOC_8007119 + themeName = White + primaryColor = #ffffff + secondaryColor = #ffffff + TEXTURE + { + mainTextureURL = BDArmory/Parts/Sidam/SIDAM_White + shader = KSP/Diffuse + } + } + } } diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/Sidam/smoke.png b/BDArmory/Distribution/GameData/BDArmory/Parts/Sidam/smoke.png new file mode 100644 index 000000000..ceb6fa6e5 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Parts/Sidam/smoke.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/Sidam/srbsmoke.png b/BDArmory/Distribution/GameData/BDArmory/Parts/Sidam/srbsmoke.png deleted file mode 100644 index 85af65b2e..000000000 Binary files a/BDArmory/Distribution/GameData/BDArmory/Parts/Sidam/srbsmoke.png and /dev/null differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/SonarPod/BDAsonarPod1A.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/SonarPod/BDAsonarPod1A.cfg index 3d7d809ee..e6f661e85 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/SonarPod/BDAsonarPod1A.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/SonarPod/BDAsonarPod1A.cfg @@ -81,7 +81,7 @@ MODULE canScan = true // scanning/detecting targets (volume search) canLock = true // locking/tracking targets (fire control) canTrackWhileScan = true // continue scanning while tracking a locked target - canRecieveRadarData = true // can work as passive data receiver (NOTE THE SPELLING! [SIC]) + canReceiveRadarData = true // can work as passive data receiver minSignalThreshold = 15 // DEPRECATED, NO LONGER USED! use detection float curve! //minLockedSignalThreshold = 90 // DEPRECATED, NO LONGER USED! use locktrack float curve! diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/StingRayBDA/StingRayBDATorpedo.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/StingRayBDA/StingRayBDATorpedo.cfg index c85d068af..8afd85179 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/StingRayBDA/StingRayBDATorpedo.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/StingRayBDA/StingRayBDATorpedo.cfg @@ -2,7 +2,7 @@ PART { name = StingRayBDATorpedo module = Part - mesh = model.mu + mesh = StingRayBDATorpedo.mu author = Spanner rescaleFactor = 1 diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/aim-120/amraam.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/aim-120/amraam.cfg index aba0415b4..35c09a700 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/aim-120/amraam.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/aim-120/amraam.cfg @@ -59,6 +59,9 @@ PART decoupleSpeed = 5 + engineFailureRate = 0 // Probability the missile engine will fail to start (0-1), evaluated once on missile launch + guidanceFailureRate = 0 // Probability the missile guidance will fail per second (0-1), evaluated every frame after launch + audioClipPath = BDArmory/Sounds/rocketLoop exhaustPrefabPath = BDArmory/Models/exhaust/smallExhaust boostExhaustPrefabPath = BDArmory/Models/exhaust/mediumExhaust @@ -66,7 +69,7 @@ PART boostTransformName = boostTransform boostExhaustTransformName = boostTransform - optimumAirspeed = 1372 //deals with how missile leads the target and calculation of turn radius for dynamic launch zone calculation, should match the max speed of your missile + optimumAirspeed = 1272 //deals with how missile leads the target and calculation of turn radius for dynamic launch zone calculation, should match the max speed of your missile aero = true //Missile has aerodynamics liftArea = 0.0020 //increases lift which helps with manuevering and turning, but also increases drag @@ -75,18 +78,21 @@ PART maxAoA = 30 //max AoA missile can turn at, will limit missile's turn radius below what is possible with maxTorque if set too low missileType = missile - homingType = aam //air to air missile different types are AAM, AGM, Cruise and Ballistic + homingType = aam //air to air missile different types are AAM, AGM, Cruise, Ballistic, ProNav, and AugProNav + // pronavGain = 3 // ProNav gain constant, only used with ProNav and AugProNav homing types (default is 3) targetingType = radar activeRadarRange = 6000 - maxOffBoresight = 120 //maximum angle, from the boresight, that the missile can track the target. This also controls how the missile can launch off boresight. When launched from the air at another air target with allAspect = false, launch off boresight is at 0.35*maxOffBoresight, otherwise it is at 0.75*,maxOffBoresight (allAspect = true OR either launching craft or target is landed/splashed). - allAspect = false // Only affects when missile can launch for radar-guided missiles, see comment on maxOffBoresight for how. For heat-seeking missiles this also allows lock-on after launch. + maxOffBoresight = 120 //maximum angle, from the boresight, that the missile can track the target. This also controls how the missile can launch off boresight. When launched from the air at another air target with uncagedLock = false, launch off boresight is at 0.35*maxOffBoresight, otherwise it is at 0.75*,maxOffBoresight (uncagedLock = true OR either launching craft or target is landed/splashed). + // allAspect = false // DEPRECATED - use uncagedLock instead + uncagedLock = false // Only affects when missile can launch for radar-guided missiles, see comment on maxOffBoresight for how. For heat-seeking missiles this also allows lock-on after launch. lockedSensorFOV = 7 //the field of view the missile can see to maintain a lock after launch, will affect accuracy + chaffEffectivity = 1 //modifies how the missile targeting is affected by chaff, 1 is fully affected (normal behavior), lower values mean less affected (i.e. 0 ignores chaff), higher values means more affected minStaticLaunchRange = 500 // minimum launch range in meters assuming craft don't move, final min launch distance is dynamically calculated based on target/launching craft speeds maxStaticLaunchRange = 25000 // maximum launch range in meters assuming craft don't move, final max launch distance is dynamically calculated based on target/launching craft speeds radarLOAL = true //radar lock on after launch - + radarTimeout = 5 //timelimit without a detected target before Active Radar guidance fails and LOAL could not lock a target (default is 5). engageAir = true engageMissile = false engageGround = false @@ -98,5 +104,6 @@ PART name = BDExplosivePart tntMass = 25 warheadType = ContinuousRod + fuseFailureRate = 0 // How often the explosive fuse will fail to detonate (0-1), evaluated once on detonation trigger } } \ No newline at end of file diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/aim-120/amraam_emp.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/aim-120/amraam_emp.cfg index 491712ad7..792dc5d82 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/aim-120/amraam_emp.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/aim-120/amraam_emp.cfg @@ -13,6 +13,7 @@ PART MODEL { model = BDArmory/Parts/aim-120/model + texture = texture, BDArmory/Parts/aim-120/textureEMP } rescaleFactor = 1 @@ -50,7 +51,7 @@ PART { name = MissileLauncher - shortName = AIM-120 + shortName = EMP AIM-120 thrust = 55 //KN thrust during boost phase cruiseThrust = 25 //thrust during cruise phase @@ -69,7 +70,7 @@ PART boostTransformName = boostTransform boostExhaustTransformName = boostTransform - optimumAirspeed = 1372 + optimumAirspeed = 1272 aero = true liftArea = 0.0020 @@ -105,5 +106,6 @@ PART { name = ModuleEMP proximity = 100 + AllowReboot = false // Allow craft to reboot and recover if true, or be permanently disabled if false, when EMP damage threshold is met or exceeded } } diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/aim-120/textureEMP.png b/BDArmory/Distribution/GameData/BDArmory/Parts/aim-120/textureEMP.png new file mode 100644 index 000000000..d1dfe0a4c Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Parts/aim-120/textureEMP.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/awacsRadar/awacsRadar.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/awacsRadar/awacsRadar.cfg index 129c90025..8f69b0da5 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/awacsRadar/awacsRadar.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/awacsRadar/awacsRadar.cfg @@ -42,6 +42,43 @@ crashTolerance = 7 maxTemp = 3600 +MODULE +{ + name = ModulePartVariants + baseVariant = Legged + useMultipleDragCubes = false + VARIANT + { + name = Legged + displayName = #loc_BDArmory_part_AWACS_Legged + primaryColor = #ffffff + secondaryColor = #ffffff + GAMEOBJECTS + { + Cube = true + Sphere = true + collider = true + legless_Sphere = false + legless_collider = false + } + } + VARIANT + { + name = Legless + displayName = #loc_BDArmory_part_AWACS_Legless + primaryColor = #ffffff + secondaryColor = #ffffff + GAMEOBJECTS + { + Cube = false + Sphere = false + collider = false + legless_Sphere = true + legless_collider = true + } + } +} + MODULE { name = ModuleRadar @@ -74,7 +111,7 @@ MODULE canScan = true // scanning/detecting targets (volume search) canLock = false // locking/tracking targets (fire control) canTrackWhileScan = false // continue scanning while tracking a locked target - canRecieveRadarData = true // can work as passive data receiver (NOTE THE SPELLING! [SIC]) + canReceiveRadarData = true // can work as passive data receiver minSignalThreshold = 50 // DEPRECATED, NO LONGER USED! use detection float curve! //minLockedSignalThreshold = 90 // DEPRECATED, NO LONGER USED! use locktrack float curve! diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/awacsRadar/model.mu b/BDArmory/Distribution/GameData/BDArmory/Parts/awacsRadar/model.mu index c839103a8..d812a8f1e 100644 Binary files a/BDArmory/Distribution/GameData/BDArmory/Parts/awacsRadar/model.mu and b/BDArmory/Distribution/GameData/BDArmory/Parts/awacsRadar/model.mu differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/browninganm2/browninganm2.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/browninganm2/browninganm2.cfg index 55ea1f963..586506614 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/browninganm2/browninganm2.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/browninganm2/browninganm2.cfg @@ -54,7 +54,7 @@ MODULE hasFireAnimation = false roundsPerMinute = 1150 - maxDeviation = 0.22 + maxDeviation = 0.32 maxEffectiveDistance = 2800 maxTargetingRange = 4000 @@ -75,7 +75,7 @@ MODULE autoProxyTrackRange = 1200 - fireSoundPath = BDArmory/Parts/browninganm2/Sounds/fire + fireSoundPath = BDArmory/Parts/browninganm2/sounds/fire overheatSoundPath = BDArmory/Parts/50CalTurret/sounds/turretOverheat oneShotSound = true diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/browninganm2/Sounds/fire.ogg b/BDArmory/Distribution/GameData/BDArmory/Parts/browninganm2/sounds/fire.ogg similarity index 100% rename from BDArmory/Distribution/GameData/BDArmory/Parts/browninganm2/Sounds/fire.ogg rename to BDArmory/Distribution/GameData/BDArmory/Parts/browninganm2/sounds/fire.ogg diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/ecmJammer/ecmj131.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/ecmJammer/ecmj131.cfg index 4370a45ff..7ee1284ac 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/ecmJammer/ecmj131.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/ecmJammer/ecmj131.cfg @@ -51,15 +51,16 @@ MODULE // Set this to true for "stealth" jammers that are integrated into Cockpits and serve // to reduce only the radar cross section, but without providing another jamming effect! - resourceDrain = 5 // EC/sec. Set this higher for more capabale jammers. - + resourceDrain = 5 // resource/sec. Set this higher for more capabale jammers. + resourceName = ElectricCharge // Resource used by the jammer. ElectricCharge by default jammerStrength = 1200 // this is a factor (in relation to a vessels base radar cross section) how much the crafts DETECTABILITY is INCREASED(!) when the jammer is active lockBreaker = true // true: jammer serves to break radar locks (default: true) lockBreakerStrength = 300 // factor (in relation to a vessels base radar cross section) how strong the lockbreaking effect is rcsReduction = false // jammer reduces a crafts radar cross section, simulating 2nd generation stealth (radar obsorbent coating) - rcsReductionFactor = 0 // factor for radar cross section: from 0 (craft is invisible) to 1 (no effect) + rcsReductionFactor = 0 // factor for radar cross section: from 0 (craft is invisible) to 1 (no effect) Greater than 1 will confer a RCS malus (craft extra visible) + cooldownInterval = -1 // if > 0, cooldown time before the device can be triggered again } } diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/gau-8/gau8.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/gau-8/gau8.cfg index 41bfbb363..fc76a8735 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/gau-8/gau8.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/gau-8/gau8.cfg @@ -57,7 +57,7 @@ PART spinDownAnimation = true SpoolUpTime = 0.3 roundsPerMinute = 3900 - maxDeviation = 0.45 + maxDeviation = 0.6405 maxEffectiveDistance = 4000 maxTargetingRange = 5000 diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/goalkeeper/goalkeeper.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/goalkeeper/goalkeeper.cfg index 241a05f71..3b40b3bde 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/goalkeeper/goalkeeper.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/goalkeeper/goalkeeper.cfg @@ -84,7 +84,7 @@ PART spinDownAnimation = true SpoolUpTime = 0.3 roundsPerMinute = 4200 - maxDeviation = 0.50 + maxDeviation = 0.675 maxEffectiveDistance = 4000 maxTargetingRange = 5000 @@ -97,6 +97,9 @@ PART bulletDrop = true weaponType = ballistic + isAPS = true + APSType = missile + dualModeAPS = true tracerLength = 0 tracerDeltaFactor = 2.75 @@ -151,7 +154,7 @@ PART canScan = true // scanning/detecting targets (volume search) canLock = true // locking/tracking targets (fire control) canTrackWhileScan = true // continue scanning while tracking a locked target - canRecieveRadarData = true // can work as passive data receiver (NOTE THE SPELLING! [SIC]) + canReceiveRadarData = true // can work as passive data receiver minSignalThreshold = 350 // DEPRECATED, NO LONGER USED! use detection float curve! minLockedSignalThreshold = 120 // DEPRECATED, NO LONGER USED! use locktrack float curve! diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/groundRadar/radar1.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/groundRadar/radar1.cfg index 0fb4260ad..cc1a2f1d4 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/groundRadar/radar1.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/groundRadar/radar1.cfg @@ -74,7 +74,7 @@ MODULE canScan = true // scanning/detecting targets (volume search) canLock = true // locking/tracking targets (fire control) canTrackWhileScan = true // continue scanning while tracking a locked target - canRecieveRadarData = true // can work as passive data receiver (NOTE THE SPELLING! [SIC]) + canReceiveRadarData = true // can work as passive data receiver minSignalThreshold = 120 // DEPRECATED, NO LONGER USED! use detection float curve! minLockedSignalThreshold = 180 // DEPRECATED, NO LONGER USED! use locktrack float curve! diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/groundRadar/radar2.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/groundRadar/radar2.cfg index cc72a441d..65f27ea7c 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/groundRadar/radar2.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/groundRadar/radar2.cfg @@ -79,7 +79,7 @@ MODULE canScan = true // scanning/detecting targets (volume search) canLock = false // locking/tracking targets (fire control) canTrackWhileScan = false // continue scanning while tracking a locked target - canRecieveRadarData = true // can work as passive data receiver (NOTE THE SPELLING! [SIC]) + canReceiveRadarData = true // can work as passive data receiver minSignalThreshold = 50 // DEPRECATED, NO LONGER USED! use detection float curve! minLockedSignalThreshold = 180 // DEPRECATED, NO LONGER USED! use locktrack float curve! diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/h70turret/h70turret.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/h70turret/h70turret.cfg index c88abf58e..3bbffed59 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/h70turret/h70turret.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/h70turret/h70turret.cfg @@ -104,18 +104,17 @@ PART MODULE { - name = BDALookConstraintUp - - targetName = pistonTransform - rotatorsName = cylinderTransform - } - - MODULE - { - name = BDALookConstraintUp - - targetName = cylinderTransform - rotatorsName = pistonTransform + name = FXModuleLookAtConstraint + CONSTRAINLOOKFX + { + targetName = pistonTransform + rotatorsName = cylinderTransform + } + CONSTRAINLOOKFX + { + targetName = cylinderTransform + rotatorsName = pistonTransform + } } RESOURCE diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/hellfireMissile/hellfireMissile.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/hellfireMissile/hellfireMissile.cfg index 37f3b303f..717ad2b8f 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/hellfireMissile/hellfireMissile.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/hellfireMissile/hellfireMissile.cfg @@ -46,7 +46,7 @@ PART { name = MissileLauncher - shortName = AGM-114R + shortName = Hellfire thrust = 10 //KN thrust during boost phase cruiseThrust = 0 //thrust during cruise phase @@ -66,8 +66,8 @@ PART targetingType = laser maxOffBoresight = 65 lockedSensorFOV = 2 - optimumAirspeed = 450 - DetonationDistance = 0.1 + optimumAirspeed = 550 + DetonationDistance = 0 agmDescentRatio = 1.1 maxAoA = 45 @@ -95,8 +95,9 @@ PART MODULE { name = BDExplosivePart - tntMass = 12 + tntMass = 8 warheadType = ShapedCharge + caliber = 172 } diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/hellfireMissile/hellfire_emp.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/hellfireMissile/hellfire_emp.cfg index 14c10af91..1e7ab9358 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/hellfireMissile/hellfire_emp.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/hellfireMissile/hellfire_emp.cfg @@ -13,6 +13,7 @@ PART MODEL { model = BDArmory/Parts/hellfireMissile/model + texture = texture, BDArmory/Parts/hellfireMissile/textureEMP } rescaleFactor = 1 @@ -49,7 +50,7 @@ PART { name = MissileLauncher - shortName = AGM-114R + shortName = EMP Hellfire thrust = 10 //KN thrust during boost phase cruiseThrust = 0 //thrust during cruise phase @@ -70,7 +71,7 @@ PART targetingType = laser maxOffBoresight = 65 lockedSensorFOV = 7 - optimumAirspeed = 450 + optimumAirspeed = 550 DetonationDistance = 0.1 agmDescentRatio = 1.1 diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/hellfireMissile/textureEMP.png b/BDArmory/Distribution/GameData/BDArmory/Parts/hellfireMissile/textureEMP.png new file mode 100644 index 000000000..b0bb32c58 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Parts/hellfireMissile/textureEMP.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/hiddenVulcan/hiddenVulcan.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/hiddenVulcan/hiddenVulcan.cfg index 6bcde6e5e..9ded0f9bd 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/hiddenVulcan/hiddenVulcan.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/hiddenVulcan/hiddenVulcan.cfg @@ -52,10 +52,10 @@ MODULE hasDeployAnim = false hasFireAnimation = false - SpoolUpTime = 0.3 + SpoolUpTime = 0.15 roundsPerMinute = 5500 - maxDeviation = 0.125 + maxDeviation = 0.88 maxEffectiveDistance = 2500 maxTargetingRange = 5000 diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/jdamMk83/jdam.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/jdamMk83/jdam.cfg index 2493f3e13..5bbff9dec 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/jdamMk83/jdam.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/jdamMk83/jdam.cfg @@ -66,8 +66,8 @@ MODULE aero = true - liftArea = 0.0009 - steerMult = .3 + liftArea = 0.004 + steerMult = 4 maxTorque = 8 engageAir = false diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/m1Abrams/m1Abrams.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/m1Abrams/m1Abrams.cfg index 293df3416..bb514c30b 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/m1Abrams/m1Abrams.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/m1Abrams/m1Abrams.cfg @@ -84,6 +84,7 @@ MODULE ammoName = CannonShells bulletType = 120mmBullet; 120mmBulletHE; 120mmBulletCannister + //bulletType = 120mmBulletSabot; 120mmBulletHEAT; 120mmBulletCannister // Delete above line and uncomment this to use realistic projectiles. requestResourceAmount = 1 canHotSwap = true diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/m1Abrams/tex_abrams.png b/BDArmory/Distribution/GameData/BDArmory/Parts/m1Abrams/tex_abrams.png index 13edb994a..b2746294c 100644 Binary files a/BDArmory/Distribution/GameData/BDArmory/Parts/m1Abrams/tex_abrams.png and b/BDArmory/Distribution/GameData/BDArmory/Parts/m1Abrams/tex_abrams.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/m230ChainGun/m230.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/m230ChainGun/m230.cfg index e3e78d906..1a47130eb 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/m230ChainGun/m230.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/m230ChainGun/m230.cfg @@ -77,7 +77,7 @@ MODULE roundsPerMinute = 625 isChaingun = true - maxDeviation = 0.4 + maxDeviation = 0.45 maxEffectiveDistance = 2500 maxTargetingRange = 5000 @@ -97,11 +97,11 @@ MODULE oneShotWorldParticles = true maxHeat = 3600 - heatPerShot = 166 + heatPerShot = 210 heatLoss = 820 - fireSoundPath = BDArmory/Parts/m230ChainGun/Sounds/m230loop - overheatSoundPath = BDArmory/Parts/m230ChainGun/Sounds/m230loopEnd + fireSoundPath = BDArmory/Parts/m230ChainGun/sounds/m230loop + overheatSoundPath = BDArmory/Parts/m230ChainGun/sounds/m230loopEnd oneShotSound = false //explosion @@ -111,34 +111,29 @@ MODULE MODULE { - name = BDALookConstraintUp - targetName = pitchPiston - rotatorsName = pitchCylinder + name = FXModuleLookAtConstraint + CONSTRAINLOOKFX + { + targetName = pitchPiston + rotatorsName = pitchCylinder + } + CONSTRAINLOOKFX + { + targetName = pitchCylinder + rotatorsName = pitchPiston + } + CONSTRAINLOOKFX + { + targetName = springTarget + rotatorsName = springHolder + } + CONSTRAINLOOKFX + { + targetName = springHolder + rotatorsName = springTarget + } } -MODULE -{ - name = BDALookConstraintUp - targetName = pitchCylinder - rotatorsName = pitchPiston -} - - -MODULE -{ - name = BDALookConstraintUp - targetName = springTarget - rotatorsName = springHolder -} - -MODULE -{ - name = BDALookConstraintUp - targetName = springHolder - rotatorsName = springTarget -} - - MODULE { name = BDAScaleByDistance diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/m230ChainGun/Sounds/m230loop.ogg b/BDArmory/Distribution/GameData/BDArmory/Parts/m230ChainGun/sounds/m230loop.ogg similarity index 100% rename from BDArmory/Distribution/GameData/BDArmory/Parts/m230ChainGun/Sounds/m230loop.ogg rename to BDArmory/Distribution/GameData/BDArmory/Parts/m230ChainGun/sounds/m230loop.ogg diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/m230ChainGun/Sounds/m230loopEnd.ogg b/BDArmory/Distribution/GameData/BDArmory/Parts/m230ChainGun/sounds/m230loopEnd.ogg similarity index 100% rename from BDArmory/Distribution/GameData/BDArmory/Parts/m230ChainGun/Sounds/m230loopEnd.ogg rename to BDArmory/Distribution/GameData/BDArmory/Parts/m230ChainGun/sounds/m230loopEnd.ogg diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/m230ChainGun/Sounds/m230shot.ogg b/BDArmory/Distribution/GameData/BDArmory/Parts/m230ChainGun/sounds/m230shot.ogg similarity index 100% rename from BDArmory/Distribution/GameData/BDArmory/Parts/m230ChainGun/Sounds/m230shot.ogg rename to BDArmory/Distribution/GameData/BDArmory/Parts/m230ChainGun/sounds/m230shot.ogg diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/maverick/maverickMissile.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/maverick/maverickMissile.cfg index a6f8fd279..c5570cb63 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/maverick/maverickMissile.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/maverick/maverickMissile.cfg @@ -64,7 +64,7 @@ PART homingType = AGM agmDescentRatio = 1.85 - optimumAirspeed = 350 + optimumAirspeed = 210 missileType = missile targetingType = laser @@ -89,6 +89,8 @@ PART { name = BDExplosivePart tntMass = 85.5 + caliber = 305 + apMod = 0.3351975632 // 950 mm of penetration warheadType = ShapedCharge } diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/missileTurret/missileTurret.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/missileTurret/missileTurret.cfg index fc58bda32..ecd9133a9 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/missileTurret/missileTurret.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/missileTurret/missileTurret.cfg @@ -117,7 +117,7 @@ PART canScan = true // scanning/detecting targets (volume search) canLock = true // locking/tracking targets (fire control) canTrackWhileScan = true // continue scanning while tracking a locked target - canRecieveRadarData = true // can work as passive data receiver (NOTE THE SPELLING! [SIC]) + canReceiveRadarData = true // can work as passive data receiver minSignalThreshold = 300 // DEPRECATED, NO LONGER USED! use detection float curve! minLockedSignalThreshold = 220 // DEPRECATED, NO LONGER USED! use locktrack float curve! diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/oMillennium/oMillennium.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/oMillennium/oMillennium.cfg index 198936011..e28e122a3 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/oMillennium/oMillennium.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/oMillennium/oMillennium.cfg @@ -11,7 +11,7 @@ PART // --- asset parameters --- mesh = model.mu - rescaleFactor = 1 + rescaleFactor = 0.9 // --- node definitions --- @@ -33,7 +33,7 @@ PART attachRules = 0,1,0,0,1 // --- standard part parameters --- - mass = 2 + mass = 1 dragModelType = default maximum_drag = 0.2 minimum_drag = 0.2 @@ -83,15 +83,20 @@ PART spinDownAnimation = false roundsPerMinute = 850 - maxDeviation = 0.47 + maxDeviation = 0.27 maxTargetingRange = 3800 airDetonation = true + detonationRange = 150 maxEffectiveDistance = 3800 weaponType = ballistic + isAPS = true + APSType = missile + dualModeAPS = true + ammoName = 30x173Ammo - bulletType = 35x228HEBullet + bulletType = 35x228AHEADBullet; 35x228HEBullet requestResourceAmount = 1 bulletDmgMult = 2.5 //so submunitions do more than scratch damage @@ -105,8 +110,8 @@ PART heatPerShot = 200 heatLoss = 740 - fireSoundPath = BDArmory/Parts/oMillennium/Sounds/oFiring - overheatSoundPath = BDArmory/Parts/oMillennium/Sounds/oFireEnd + fireSoundPath = BDArmory/Parts/oMillennium/sounds/oFiring + overheatSoundPath = BDArmory/Parts/oMillennium/sounds/oFireEnd oneShotSound = false showReloadMeter = false explModelPath = BDArmory/Models/explosion/30mmExplosion diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/oMillennium/Sounds/oFireEnd.ogg b/BDArmory/Distribution/GameData/BDArmory/Parts/oMillennium/sounds/oFireEnd.ogg similarity index 100% rename from BDArmory/Distribution/GameData/BDArmory/Parts/oMillennium/Sounds/oFireEnd.ogg rename to BDArmory/Distribution/GameData/BDArmory/Parts/oMillennium/sounds/oFireEnd.ogg diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/oMillennium/Sounds/oFiring.ogg b/BDArmory/Distribution/GameData/BDArmory/Parts/oMillennium/sounds/oFiring.ogg similarity index 100% rename from BDArmory/Distribution/GameData/BDArmory/Parts/oMillennium/Sounds/oFiring.ogg rename to BDArmory/Distribution/GameData/BDArmory/Parts/oMillennium/sounds/oFiring.ogg diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/pac-3/pac3.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/pac-3/pac3.cfg index 8b232bb6f..56067ed6c 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/pac-3/pac3.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/pac-3/pac3.cfg @@ -62,7 +62,7 @@ PART exhaustPrefabPath = BDArmory/Models/exhaust/mediumExhaust - optimumAirspeed = 1250 + optimumAirspeed = 1280 aero = true liftArea = 0.005 diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/patriotLauncher/patriotLauncher.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/patriotLauncher/patriotLauncher.cfg index 1c949343b..744436216 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/patriotLauncher/patriotLauncher.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/patriotLauncher/patriotLauncher.cfg @@ -106,20 +106,16 @@ PART MODULE { - name = BDALookConstraintUp - - targetName = pistonTransform - rotatorsName = cylinderTransform + name = FXModuleLookAtConstraint + CONSTRAINLOOKFX + { + targetName = pistonTransform + rotatorsName = cylinderTransform + } + CONSTRAINLOOKFX + { + targetName = cylinderTransform + rotatorsName = pistonTransform + } } - - MODULE - { - name = BDALookConstraintUp - - targetName = cylinderTransform - rotatorsName = pistonTransform - } - - - } diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/radarDataReceiver/radarDataReceiver.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/radarDataReceiver/radarDataReceiver.cfg index 8477c8a5f..7095deb1c 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/radarDataReceiver/radarDataReceiver.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/radarDataReceiver/radarDataReceiver.cfg @@ -76,7 +76,7 @@ MODULE canScan = false // scanning/detecting targets (volume search) canLock = false // locking/tracking targets (fire control) canTrackWhileScan = false // continue scanning while tracking a locked target - canRecieveRadarData = true // can work as passive data receiver (NOTE THE SPELLING! [SIC]) + canReceiveRadarData = true // can work as passive data receiver //minSignalThreshold = 350 // DEPRECATED, NO LONGER USED! use detection float curve! //minLockedSignalThreshold = 120 // DEPRECATED, NO LONGER USED! use locktrack float curve! diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/radome125/radome1.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/radome125/radome1.cfg index 5bdb0efd9..7f603f31e 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/radome125/radome1.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/radome125/radome1.cfg @@ -31,6 +31,7 @@ PART MODEL { model = BDArmory/Parts/radome125/radome1 + texture = tex_radome125, BDArmory/Parts/radome125/tex_radome125 } @@ -66,7 +67,7 @@ MODULE canScan = true // scanning/detecting targets (volume search) canLock = true // locking/tracking targets (fire control) canTrackWhileScan = true // continue scanning while tracking a locked target - canRecieveRadarData = false // can work as passive data receiver (NOTE THE SPELLING! [SIC]) + canReceiveRadarData = false // can work as passive data receiver minSignalThreshold = 80 // DEPRECATED, NO LONGER USED! use detection float curve! minLockedSignalThreshold = 100 // DEPRECATED, NO LONGER USED! use locktrack float curve! diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/radome125/radome1GA.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/radome125/radome1GA.cfg new file mode 100644 index 000000000..b44721ed7 --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/radome125/radome1GA.cfg @@ -0,0 +1,87 @@ +PART +{ + name = bdRadome1GA + module = Part + author = BahamutoD + rescaleFactor = 1 + node_stack_bottom01 = 0.0, -0.9382, 0.0, 0.0, -1.0, 0.0, 1 + //node_attach = 0, 0, -0.313, 0.0, 0.0, 1.0 + TechRequired = precisionEngineering + entryCost = 5500 + cost = 2000 + category = none + bdacategory = Radars + subcategory = 0 + bulkheadProfiles = size1 + title = #loc_BDArmory_part_bdRadome1_ground_title //APG-77v1 air-to-ground Radar + manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics + description = #loc_BDArmory_part_bdRadome1_Gnd_desc //The AN/APG-77v1 is a forward facing, aerodynamically housed, solid-state, active electronically scanned array (AESA) radar. It provides full air-to-ground functionality at a MAX operating range of 40km against stationary and moving targets within a 120 degree field of view. This particular unit is optimized for Ground combat, and has difficulties locking air targets. + attachRules = 1,0,1,1,0 + stackSymmetry = 2 + mass = 0.375 + dragModelType = default + maximum_drag = 0.1 + minimum_drag = 0.1 + angularDrag = .25 + crashTolerance = 40 + maxTemp = 2000 + fuelCrossFeed = True + thermalMassModifier = 6.0 + emissiveConstant = 0.95 + MODEL + { + model = BDArmory/Parts/radome125/radome1 + texture = tex_radome125, BDArmory/Parts/radome125/tex_radome125_ground + } + + + MODULE + { + name = ModuleRadar + + // -- Section: General Configuration -- + radarName = APG-77 atg + rwrThreatType = 1 + rotationTransformName = scanRotation + resourceDrain = 0.825 + // -- Section: Capabilities -- + omnidirectional = false + directionalFieldOfView = 120 + lockRotationAngle = 4 + showDirectionWhileScan = true + multiLockFOV = 40 + //lockAttemptFOV = 2 + maxLocks = 3 + canScan = true + canLock = true + canTrackWhileScan = true + canReceiveRadarData = false + radarGroundClutterFactor = 1.7 + + radarDetectionCurve + { + key = 0 0 0 0 + key = 5 0.9 0.29 0.31 + key = 10 3 0.48 0.51 + key = 15 5.9 0.62 0.69 + key = 20 10 0.96 0.9 + key = 25 14.1 0.71 0.71 + key = 30 17.3 0.58 0.58 + key = 35 20 0.48 0.61 + key = 40 35 9.18 1.5 + } + + radarLockTrackCurve + { + key = 0 0 0 0 + key = 5 0.9 0.47 0.44 + key = 10 3.5 0.59 0.59 + key = 15 7 0.73 0.71 + key = 20 11 0.79 0.9 + key = 25 16 1.05 1.05 + key = 30 21 1.09 0.9 + key = 35 25 0.48 0.49 + key = 40 40 9.18 1.5 + } + } +} diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/radome125/radome1inline.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/radome125/radome1inline.cfg index 9a07185e2..4a95cd8bd 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/radome125/radome1inline.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/radome125/radome1inline.cfg @@ -32,6 +32,7 @@ PART MODEL { model = BDArmory/Parts/radome125/radome1inline + texture = tex_radome125, BDArmory/Parts/radome125/tex_radome125_inline } @@ -67,7 +68,7 @@ MODULE canScan = true // scanning/detecting targets (volume search) canLock = true // locking/tracking targets (fire control) canTrackWhileScan = true // continue scanning while tracking a locked target - canRecieveRadarData = false // can work as passive data receiver (NOTE THE SPELLING! [SIC]) + canReceiveRadarData = false // can work as passive data receiver minSignalThreshold = 80 // DEPRECATED, NO LONGER USED! use detection float curve! minLockedSignalThreshold = 100 // DEPRECATED, NO LONGER USED! use locktrack float curve! diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/radome125/radome1inlineGA.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/radome125/radome1inlineGA.cfg new file mode 100644 index 000000000..648bb416e --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/radome125/radome1inlineGA.cfg @@ -0,0 +1,86 @@ +PART +{ + name = bdRadome1inlineGA + module = Part + author = BahamutoD + rescaleFactor = 1 + node_stack_bottom01 = 0.0, -0.6598, 0.0, 0.0, -1.0, 0.0, 1 + node_stack_top = 0.0, 0.6598, 0.0, 0.0, 1.0, 0.0, 1 + node_attach = 0, 0, -0.625, 0.0, 0.0, 1.0 + TechRequired = precisionEngineering + entryCost = 5500 + cost = 2000 + category = none + bdacategory = Radars + subcategory = 0 + bulkheadProfiles = size1 + title = #loc_BDArmory_part_bdRadome1inline_ground_title //APG-77v1 air-to-ground Radar (Inline) + manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics + description = #loc_BDArmory_part_bdRadome1_Gnd_desc //The AN/APG-77v1 is a forward facing, aerodynamically housed, solid-state, active electronically scanned array (AESA) radar. It provides full air-to-ground functionality at a MAX operating range of 40km against stationary and moving targets within a 120 degree field of view. This particular unit is optimized for Ground combat, and has difficulties locking air targets._ + attachRules = 1,1,1,1,0 + stackSymmetry = 2 + mass = 0.375 + dragModelType = default + maximum_drag = 0.1 + minimum_drag = 0.1 + angularDrag = .25 + crashTolerance = 40 + maxTemp = 2000 + fuelCrossFeed = True + thermalMassModifier = 6.0 + emissiveConstant = 0.95 + MODEL + { + model = BDArmory/Parts/radome125/radome1inline + texture = tex_radome125, BDArmory/Parts/radome125/tex_radome125_inline_ground + } + + + MODULE + { + name = ModuleRadar + + // -- Section: General Configuration -- + radarName = APG-77 atg + rwrThreatType = 1 + rotationTransformName = scanRotation + resourceDrain = 0.825 + // -- Section: Capabilities -- + omnidirectional = false + directionalFieldOfView = 120 + lockRotationAngle = 4 + showDirectionWhileScan = true + multiLockFOV = 40 + maxLocks = 3 + canScan = true + canLock = true + canTrackWhileScan = true + canReceiveRadarData = false + radarGroundClutterFactor = 1.7 + radarDetectionCurve + { + key = 0 0 0 0 + key = 5 0.9 0.29 0.31 + key = 10 3 0.48 0.51 + key = 15 5.9 0.62 0.69 + key = 20 10 0.96 0.9 + key = 25 14.1 0.71 0.71 + key = 30 17.3 0.58 0.58 + key = 35 20 0.48 0.61 + key = 40 35 9.18 1.5 + } + + radarLockTrackCurve + { + key = 0 0 0 0 + key = 5 0.9 0.47 0.44 + key = 10 3.5 0.59 0.59 + key = 15 7 0.73 0.71 + key = 20 11 0.79 0.9 + key = 25 16 1.05 1.05 + key = 30 21 1.09 0.9 + key = 35 25 0.48 0.49 + key = 40 40 9.18 1.5 + } + } +} diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/radome125/radome1snub.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/radome125/radome1snub.cfg index b44966f00..110ae4f8b 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/radome125/radome1snub.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/radome125/radome1snub.cfg @@ -31,6 +31,7 @@ PART MODEL { model = BDArmory/Parts/radome125/radome1snub + texture = tex_radome125, BDArmory/Parts/radome125/tex_radome125 } @@ -66,7 +67,7 @@ MODULE canScan = true // scanning/detecting targets (volume search) canLock = true // locking/tracking targets (fire control) canTrackWhileScan = true // continue scanning while tracking a locked target - canRecieveRadarData = false // can work as passive data receiver (NOTE THE SPELLING! [SIC]) + canReceiveRadarData = false // can work as passive data receiver minSignalThreshold = 80 // DEPRECATED, NO LONGER USED! use detection float curve! minLockedSignalThreshold = 100 // DEPRECATED, NO LONGER USED! use locktrack float curve! diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/radome125/radome1snubGA.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/radome125/radome1snubGA.cfg new file mode 100644 index 000000000..75e3ad9a0 --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/radome125/radome1snubGA.cfg @@ -0,0 +1,83 @@ +PART +{ + name = bdRadome1snubGA + module = Part + author = BahamutoD + rescaleFactor = 1 + node_stack_bottom01 = 0.0, -0.4816, 0.0, 0.0, -1.0, 0.0, 1 + //node_attach = 0, 0, -0.313, 0.0, 0.0, 1.0 + TechRequired = precisionEngineering + entryCost = 5500 + cost = 2250 + category = none + bdacategory = Radars + subcategory = 0 + bulkheadProfiles = size1 + title = title = #loc_BDArmory_part_bdRadome1snub_ground_title //APG-77v1 air-to-ground Radar (Snub) + manufacturer = #loc_BDArmory_agent_title //Bahamuto Dynamics + description = #loc_BDArmory_part_bdRadome1_Gnd_desc //The AN/APG-77v1 is a forward facing, aerodynamically housed, solid-state, active electronically scanned array (AESA) radar. It provides full air-to-ground functionality at a MAX operating range of 40km against stationary and moving targets within a 120 degree field of view. This particular unit is optimized for Ground combat, and has difficulties locking air targets. + attachRules = 1,0,1,1,0 + stackSymmetry = 2 + mass = 0.375 + dragModelType = default + maximum_drag = 0.1 + minimum_drag = 0.1 + angularDrag = .25 + crashTolerance = 40 + maxTemp = 2000 + fuelCrossFeed = True + thermalMassModifier = 6.0 + emissiveConstant = 0.95 + MODEL + { + model = BDArmory/Parts/radome125/radome1snub + texture = tex_radome125, BDArmory/Parts/radome125/tex_radome125_ground + } + + MODULE + { + name = ModuleRadar + + // -- Section: General Configuration -- + radarName = APG-77 atg + rwrThreatType = 1 + rotationTransformName = scanRotation + resourceDrain = 0.75 + // -- Section: Capabilities -- + omnidirectional = false + directionalFieldOfView = 120 + lockRotationAngle = 4 + showDirectionWhileScan = true + multiLockFOV = 40 + maxLocks = 1 + canScan = true + canLock = true + canTrackWhileScan = true + canReceiveRadarData = false + radarGroundClutterFactor = 1.7 + radarDetectionCurve + { + key = 0 0 0 0 + key = 5 0.9 0.29 0.31 + key = 10 3 0.48 0.51 + key = 15 5.9 0.62 0.69 + key = 20 10 0.96 0.9 + key = 25 14.1 0.71 0.71 + key = 30 17.3 0.58 0.58 + key = 35 20 0.48 0.61 + key = 40 35 9.18 1.5 + } + radarLockTrackCurve + { + key = 0 0 0 0 + key = 5 0.9 0.47 0.44 + key = 10 3.5 0.59 0.59 + key = 15 7 0.73 0.71 + key = 20 11 0.79 0.9 + key = 25 16 1.05 1.05 + key = 30 21 1.09 0.9 + key = 35 25 0.48 0.49 + key = 40 40 9.18 1.5 + } + } +} diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/radome125/tex_radome125_ground.png b/BDArmory/Distribution/GameData/BDArmory/Parts/radome125/tex_radome125_ground.png new file mode 100644 index 000000000..0b825a5d2 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Parts/radome125/tex_radome125_ground.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/radome125/tex_radome125_inline.png b/BDArmory/Distribution/GameData/BDArmory/Parts/radome125/tex_radome125_inline.png new file mode 100644 index 000000000..1c0bf4c32 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Parts/radome125/tex_radome125_inline.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/radome125/tex_radome125_inline_ground.png b/BDArmory/Distribution/GameData/BDArmory/Parts/radome125/tex_radome125_inline_ground.png new file mode 100644 index 000000000..299490aac Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Parts/radome125/tex_radome125_inline_ground.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/rotaryBombBay/model.mu b/BDArmory/Distribution/GameData/BDArmory/Parts/rotaryBombBay/model.mu index 1a4f02a7d..4e6ac7b08 100644 Binary files a/BDArmory/Distribution/GameData/BDArmory/Parts/rotaryBombBay/model.mu and b/BDArmory/Distribution/GameData/BDArmory/Parts/rotaryBombBay/model.mu differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/sidewinder/sidewinder.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/sidewinder/sidewinder.cfg index 3b5c1fc95..51817587f 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/sidewinder/sidewinder.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/sidewinder/sidewinder.cfg @@ -71,12 +71,19 @@ PART //aeroSteerDamping = 4.5 torqueRampUp = 50 + engineFailureRate = 0 // Probability the missile engine will fail to start (0-1), evaluated once on missile launch + guidanceFailureRate = 0 // Probability the missile guidance will fail per second (0-1), evaluated every frame after launch + homingType = aam missileType = missile targetingType = heat heatThreshold = 50 // Distance-adjusted heat of target in order to be detected by missile, lower value results in better detection range - maxOffBoresight = 90 //maximum angle, from the boresight, that the missile can track the target. This also controls how the missile can launch off boresight. When launched from the air at another air target with allAspect = false, launch off boresight is at 0.35*maxOffBoresight, otherwise it is at 0.75*,maxOffBoresight (allAspect = true OR either launching craft or target is landed/splashed). - allAspect = false // true: Allows missile to be cued to a radar target, also controls launch conditions, see above + frontAspectHeatModifier = 1 // Modifies the heat signature of craft when ASPECTED_IR_SEEKERS = true in BDA settings.cfg. Useful for making rear-aspect only heaters (set frontAspectHeatModifier < 1), ex: + // Heat value within 50 deg cone of non-prop engine exhaust: (Engine Heat) + // Heat value outside of 50 deg cone of engine exhaust (or a prop engine): (Engine Heat) * frontAspectHeatModifier * (Internally Calculated Occlusion Factor) + maxOffBoresight = 90 //maximum angle, from the boresight, that the missile can track the target. This also controls how the missile can launch off boresight. When launched from the air at another air target with uncagedLock = false, launch off boresight is at 0.35*maxOffBoresight, otherwise it is at 0.75*,maxOffBoresight (uncagedLock = true OR either launching craft or target is landed/splashed). + uncagedLock = false // true: Allows missile to be cued to a radar target, also controls launch conditions, see above + // allAspect = false // DEPRECATED - use uncagedLock instead lockedSensorFOV = 6 // How much the seeker can see in the direction it is looking (in terms of angle cone). lockedSensorFOVBias { @@ -137,5 +144,6 @@ PART name = BDExplosivePart tntMass = 15 warheadType = ContinuousRod + fuseFailureRate = 0 // How often the explosive fuse will fail to detonate (0-1), evaluated once on detonation trigger } } \ No newline at end of file diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/smallWarhead/part.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/smallWarhead/part.cfg index 262fddd1f..765ef4e7c 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/smallWarhead/part.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/smallWarhead/part.cfg @@ -53,6 +53,7 @@ maxTemp = 3400 { name = BDExplosivePart tntMass = 150 + fuseFailureRate = 0 // How often the explosive fuse will fail to detonate (0-1), evaluated once on detonation trigger } } diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/towMissile/towMissile.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/towMissile/towMissile.cfg index ab3966ead..4f913f657 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/towMissile/towMissile.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/towMissile/towMissile.cfg @@ -61,7 +61,7 @@ PART decoupleSpeed = 10 decoupleForward = true - optimumAirspeed = 320 + optimumAirspeed = 348 homingType = BeamRiding targetingType = laser @@ -84,7 +84,7 @@ PART maxStaticLaunchRange = 3750 audioClipPath = BDArmory/Sounds/rocketLoop boostClipPath = BDArmory/Sounds/rocketLoop - exhaustPrefabPath = BDArmory/Models/exhaust/smallExhaust + //exhaustPrefabPath = BDArmory/Models/exhaust/smallExhaust boostTransformName = boostTransform engageAir = false @@ -97,7 +97,9 @@ PART MODULE { name = BDExplosivePart - tntMass = 10 + //tntMass = 6.14 // TOW-2B Warhead + tntMass = 3.9 // TOW Warhead + caliber = 152 warheadType = ShapedCharge } diff --git a/BDArmory/Distribution/GameData/BDArmory/Parts/weaponManager/weaponManager.cfg b/BDArmory/Distribution/GameData/BDArmory/Parts/weaponManager/weaponManager.cfg index b9a5d68e8..636383578 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Parts/weaponManager/weaponManager.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Parts/weaponManager/weaponManager.cfg @@ -52,6 +52,7 @@ PART MODULE { name = RadarWarningReceiver + omniDetection = true } MODULE diff --git a/BDArmory/Distribution/GameData/BDArmory/Resources/BDAmmo_Universal.cfg b/BDArmory/Distribution/GameData/BDArmory/Resources/BDAmmo_Universal.cfg index 90a00e59c..1afb47e38 100644 --- a/BDArmory/Distribution/GameData/BDArmory/Resources/BDAmmo_Universal.cfg +++ b/BDArmory/Distribution/GameData/BDArmory/Resources/BDAmmo_Universal.cfg @@ -208,7 +208,7 @@ RESOURCE_DEFINITION name = 57x438Ammo title = 57x438 mm Ammo abbreviation = 57/438 -density = .00151 +density = .005675 flowMode = ALL_VESSEL transfer = PUMP isTweakable = true diff --git a/BDArmory/Distribution/GameData/BDArmory/Sounds/Jet.ogg b/BDArmory/Distribution/GameData/BDArmory/Sounds/jet.ogg similarity index 100% rename from BDArmory/Distribution/GameData/BDArmory/Sounds/Jet.ogg rename to BDArmory/Distribution/GameData/BDArmory/Sounds/jet.ogg diff --git a/BDArmory/Distribution/GameData/BDArmory/Textures/IRspike.png b/BDArmory/Distribution/GameData/BDArmory/Textures/IRspike.png new file mode 100644 index 000000000..a8f92970c Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Textures/IRspike.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Textures/friendlyIRContactIcon.png b/BDArmory/Distribution/GameData/BDArmory/Textures/friendlyIRContactIcon.png new file mode 100644 index 000000000..400abcf1e Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Textures/friendlyIRContactIcon.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Textures/icon_vm.png b/BDArmory/Distribution/GameData/BDArmory/Textures/icon_vm.png new file mode 100644 index 000000000..02be34f91 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Textures/icon_vm.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Textures/irContactIcon.png b/BDArmory/Distribution/GameData/BDArmory/Textures/irContactIcon.png new file mode 100644 index 000000000..37980b03a Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Textures/irContactIcon.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Textures/omniIRSTScanTexture.png b/BDArmory/Distribution/GameData/BDArmory/Textures/omniIRSTScanTexture.png new file mode 100644 index 000000000..98bddce16 Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Textures/omniIRSTScanTexture.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/Textures/redDot.png b/BDArmory/Distribution/GameData/BDArmory/Textures/redDot.png new file mode 100644 index 000000000..cf5121d7f Binary files /dev/null and b/BDArmory/Distribution/GameData/BDArmory/Textures/redDot.png differ diff --git a/BDArmory/Distribution/GameData/BDArmory/craft/BDA - Test Platform.craft b/BDArmory/Distribution/GameData/BDArmory/craft/BDA - Test Platform.craft index c5b59154e..cc5dd5f2f 100644 --- a/BDArmory/Distribution/GameData/BDArmory/craft/BDA - Test Platform.craft +++ b/BDArmory/Distribution/GameData/BDArmory/craft/BDA - Test Platform.craft @@ -4919,7 +4919,7 @@ PART } MODULE { - name = BDALookConstraintUp + name = FXModuleLookAtConstraint isEnabled = True stagingEnabled = True EVENTS @@ -4934,7 +4934,7 @@ PART } MODULE { - name = BDALookConstraintUp + name = FXModuleLookAtConstraint isEnabled = True stagingEnabled = True EVENTS @@ -4949,7 +4949,7 @@ PART } MODULE { - name = BDALookConstraintUp + name = FXModuleLookAtConstraint isEnabled = True stagingEnabled = True EVENTS @@ -4964,7 +4964,7 @@ PART } MODULE { - name = BDALookConstraintUp + name = FXModuleLookAtConstraint isEnabled = True stagingEnabled = True EVENTS @@ -5206,7 +5206,7 @@ PART } MODULE { - name = BDALookConstraintUp + name = FXModuleLookAtConstraint isEnabled = True stagingEnabled = True EVENTS @@ -5221,7 +5221,7 @@ PART } MODULE { - name = BDALookConstraintUp + name = FXModuleLookAtConstraint isEnabled = True stagingEnabled = True EVENTS diff --git a/BDArmory/Distribution/GameData/BDArmory/craft/SpawnProbe.craft b/BDArmory/Distribution/GameData/BDArmory/craft/SpawnProbe.craft index ca03b763b..cb955a8ef 100644 --- a/BDArmory/Distribution/GameData/BDArmory/craft/SpawnProbe.craft +++ b/BDArmory/Distribution/GameData/BDArmory/craft/SpawnProbe.craft @@ -151,23 +151,6 @@ PART } } MODULE - { - name = ModuleCargoPart - isEnabled = True - beingAttached = False - beingSettled = False - stagingEnabled = True - EVENTS - { - } - ACTIONS - { - } - UPGRADESAPPLIED - { - } - } - MODULE { name = HitpointTracker isEnabled = True diff --git a/BDArmory/Distribution/GameData/BDArmory/parse_CS_log_files.py b/BDArmory/Distribution/GameData/BDArmory/parse_CS_log_files.py index e062d83f5..f4bef6fe0 100644 --- a/BDArmory/Distribution/GameData/BDArmory/parse_CS_log_files.py +++ b/BDArmory/Distribution/GameData/BDArmory/parse_CS_log_files.py @@ -4,12 +4,12 @@ import sys from pathlib import Path -VERSION = "2.0" +VERSION = "2.3" -parser = argparse.ArgumentParser(description="Log-file parser for continuous spawning logs.", formatter_class=argparse.ArgumentDefaultsHelpFormatter) -parser.add_argument("logs", nargs='*', help="Log-files to parse. If none are given, all valid log-files are parsed.") +parser = argparse.ArgumentParser(description="Log file parser for continuous spawning logs.", formatter_class=argparse.ArgumentDefaultsHelpFormatter) +parser.add_argument("logs", nargs='*', help="Log files to parse. If none are given, the latest log file is parsed.") parser.add_argument("-n", "--no-file", action='store_true', help="Don't create a csv file.") -parser.add_argument("-w", "--weights", type=str, default="2,1,-1,0.02,2e-5,0.05,2e-3,1e-4,0.5,0.01,2e-5,0,0", help="Score weights.") +parser.add_argument("-w", "--weights", type=str, default="3,1.5,-1,4e-3,1e-4,4e-5,0.035,6e-4,1.5e-4, 5e-5,0.15,2e-3,3e-5,1.5e-5,0.075,0,0", help="Score weights.") parser.add_argument("--show-weights", action='store_true', help="Show the score weights.") parser.add_argument("--version", action='store_true', help="Show the script version, then exit.") args = parser.parse_args() @@ -21,7 +21,8 @@ log_dir = Path(__file__).parent / "Logs" if len(args.logs) == 0 else Path('.') output_log_file = log_dir / "results.csv" -fields = ["kills", "assists", "deaths", "hits", "bullet damage", "rocket strikes", "rocket parts hit", "rocket damage", "missile strikes", "missile parts hit", "missile damage", "accuracy", "rocket accuracy"] +fields = ["kills", "assists", "deaths", "hits", "bullet damage", "bullet damage taken", "rocket strikes", "rocket parts hit", "rocket damage", "rocket damage taken", "missile strikes", "missile parts hit", "missile damage", "missile damage taken", "rammed parts", "accuracy", "rocket accuracy"] +fields_short = {field: field_short for field, field_short in zip(fields, ["Kills", "Assists", "Deaths", "Hits", "Damage", "DmgTkn", "RktHits", "RktParts", "RktDmg", "RktDmgTkn", "MisHits", "MisParts", "MisDmg", "MisDmgTkn", "Ram", "Acc%", "RktAcc%"])} try: weights = {field: float(w) for field, w in zip(fields, args.weights.split(','))} if len(weights) != len(fields): @@ -35,8 +36,14 @@ print(f"{f}:{' '*(field_width - len(f))} {w}") sys.exit() +if len(args.logs) > 0: + competition_files = [Path(filename) for filename in args.logs if filename.endswith(".log")] +else: + competition_files = list(sorted(list(log_dir.glob("cts-*.log")))) + if len(competition_files) > 0: + competition_files = competition_files[-1:] + data = {} -competition_files = [Path(filename) for filename in args.logs if filename.endswith(".log")] if len(args.logs) > 0 else [filename for filename in Path.iterdir(log_dir) if filename.suffix in (".log", ".txt")] # Pre-scan the files in case something changes (iterators don't like that). for filename in competition_files: with open(log_dir / filename if len(args.logs) == 0 else filename, "r") as file_data: Craft_Name = None @@ -48,7 +55,7 @@ data[Craft_Name] = {"kills": 0, "assists": 0, "deaths": 0, "hits": 0, "bullet damage": 0, "acc hits": 0, "shots": 0, "accuracy": 0, "rocket strikes": 0, "rocket parts hit": 0, "rocket damage": 0, "acc rocket strikes": 0, "rockets fired": 0, "rocket accuracy": 0, "missile strikes": 0, "missile parts hit": 0, "missile damage": 0, "score": 0, "damage/spawn": 0} elif " DEATHCOUNT:" in line: # Counts up deaths data[Craft_Name]["deaths"] = int(line.split("DEATHCOUNT:")[-1].replace("\n", "")) - elif (m:= re.match(".*CLEAN[^:]*:", line)) is not None: # Counts up clean kills, frags, explodes and rams + elif (m := re.match(".*CLEAN[^:]*:", line)) is not None: # Counts up clean kills, frags, explodes and rams killedby = {int(nr): killer for nr, killer in (cleanKill.split(":") for cleanKill in m.string[m.end():].replace("\n", "").split(", "))} if "killed by" in data[Craft_Name]: data[Craft_Name]["killed by"].update(killedby) @@ -96,6 +103,10 @@ data[Craft_Name]["rocket damage"] = sum(damageby[life][Craft_Name] for damageby in (data[other]["rocket damage by"] for other in data if other != Craft_Name and "rocket damage by" in data[other]) for life in damageby if Craft_Name in damageby[life]) data[Craft_Name]["missile damage"] = sum(damageby[life][Craft_Name] for damageby in (data[other]["missile damage by"] for other in data if other != Craft_Name and "missile damage by" in data[other]) for life in damageby if Craft_Name in damageby[life]) + data[Craft_Name]["bullet damage taken"] = sum(damage for damageby in data[Craft_Name]['bullet damage by'].values() for damage in damageby.values()) if 'bullet damage by' in data[Craft_Name] else 0 + data[Craft_Name]["rocket damage taken"] = sum(damage for damageby in data[Craft_Name]['rocket damage by'].values() for damage in damageby.values()) if 'rocket damage by' in data[Craft_Name] else 0 + data[Craft_Name]["missile damage taken"] = sum(damage for damageby in data[Craft_Name]['missile damage by'].values() for damage in damageby.values()) if 'missile damage by' in data[Craft_Name] else 0 + data[Craft_Name]["rammed parts"] = sum(partshitby[life][Craft_Name] for partshitby in (data[other]["rammed by"] for other in data if other != Craft_Name and "rammed by" in data[other]) for life in partshitby if Craft_Name in partshitby[life]) data[Craft_Name]["damage/spawn"] = (data[Craft_Name]["bullet damage"] + data[Craft_Name]["rocket damage"] + data[Craft_Name]["missile damage"]) / (1 + data[Craft_Name]["deaths"]) @@ -143,8 +154,8 @@ if len(data) > 0: # Write results to console name_length = max([len(craft) for craft in data]) - field_lengths = {field: max(len(field) + 2, 8) for field in fields} - print(f"Name{' '*(name_length-4)} score" + "".join(f"{field:>{field_lengths[field]}}" for field in fields)) + field_lengths = {field: max(len(fields_short[field]) + 2, 8) for field in fields} + print(f"Name{' '*(name_length-4)} score" + "".join(f"{fields_short[field]:>{field_lengths[field]}}" for field in fields)) for craft in sorted(data, key=lambda c: data[c]["score"], reverse=True): print(f"{craft}{' '*(name_length-len(craft))} {data[craft]['score']:8.2f}" + "".join(f"{data[craft][field]:>{field_lengths[field]}.0f}" if 'accuracy' not in field else f"{data[craft][field]:>{field_lengths[field]-1}.1f}%" for field in fields)) diff --git a/BDArmory/Distribution/GameData/BDArmory/parse_n-choose-k_results.py b/BDArmory/Distribution/GameData/BDArmory/parse_n-choose-k_results.py index 17b183655..d9162b5df 100644 --- a/BDArmory/Distribution/GameData/BDArmory/parse_n-choose-k_results.py +++ b/BDArmory/Distribution/GameData/BDArmory/parse_n-choose-k_results.py @@ -7,11 +7,12 @@ from collections import Counter from pathlib import Path -VERSION = "1.0" +VERSION = "1.1" parser = argparse.ArgumentParser(description="Parse results.json of a N-choose-K style tournament producing a table of who-beat-who.", formatter_class=argparse.ArgumentDefaultsHelpFormatter, epilog="Note: this also works on FFA style tournaments, but may not be meaningful.") parser.add_argument('results', type=str, nargs='?', help="results.json file to parse.") parser.add_argument('-o', '--output', default="n-choose-k.csv", help="File to output CSV to.") +parser.add_argument('--tsv', action='store_true', help="Output to a TSV (tab-separated values) file instead of a CSV file.") parser.add_argument("--version", action='store_true', help="Show the script version, then exit.") args = parser.parse_args() @@ -28,7 +29,6 @@ if len(tournamentFolders) > 0: args.results = tournamentFolders[-1] / "results.json" # Results in latest tournament dir - if args.results is not None: results_file = Path(args.results) if not results_file.exists(): @@ -44,9 +44,13 @@ t = [[0] * len(names) for i in range(len(names))] for k, c in counts.items(): t[name_map[k[0]]][name_map[k[1]]] = c - with open(args.output, 'w') as f: - f.write("vs," + ",".join(names) + ",,sum(wins)\n") + output_file = Path(args.output) + if args.tsv: + output_file = output_file.with_suffix(".tsv") + with open(output_file, 'w') as f: + separator = "," if not args.tsv else "\t" + f.write("vs" + separator + separator.join(names) + separator * 2 + "sum(wins)\n") for i, name in enumerate(names): - f.write(name + "," + ",".join([str(c) for c in t[i]]) + ",," + str(sum(t[i])) + "\n") - f.write(","*(len(names)+2)) - f.write("\nsum(losses)," + ",".join([str(sum(t[i][j] for i in range(len(names)))) for j in range(len(names))])+",,\n") + f.write(name + separator + separator.join([str(c) for c in t[i]]) + separator * 2 + str(sum(t[i])) + "\n") + f.write(separator * (len(names) + 2)) + f.write("\nsum(losses)" + separator + separator.join([str(sum(t[i][j] for i in range(len(names)))) for j in range(len(names))]) + separator * 2 + "\n") diff --git a/BDArmory/Distribution/GameData/BDArmory/parse_pvp_scores.py b/BDArmory/Distribution/GameData/BDArmory/parse_pvp_scores.py new file mode 100644 index 000000000..7f1ad55bd --- /dev/null +++ b/BDArmory/Distribution/GameData/BDArmory/parse_pvp_scores.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python3 + +# Standard library imports +import argparse +import json +import math +import sys +import traceback +from pathlib import Path +from typing import Union + +# Third party imports +import matplotlib.pyplot as plt + +VERSION = "1.2.0" + +parser = argparse.ArgumentParser(description="PVP score parser", formatter_class=argparse.ArgumentDefaultsHelpFormatter) +parser.add_argument('tournament', type=str, nargs='*', help="Tournament folder to parse.") +parser.add_argument('-c', '--current-dir', action='store_true', help="Parse the logs in the current directory as if it was a tournament without the folder structure.") +parser.add_argument('--csv', action='store_true', help="Create a CSV file with the PVP scores for the entire tournament.") +parser.add_argument('--plot', action='store_true', help="Plot a diagram with of the overall PVP scores.") +parser.add_argument("--version", action='store_true', help="Show the script version, then exit.") +args = parser.parse_args() + +if args.version: + print(f"Version: {VERSION}") + sys.exit() + + +def CalculateAccuracy(hits, shots): return 100 * hits / shots if shots > 0 else 0 + + +def CalculateAvgHP(hp, heats): return hp / heats if heats > 0 else 0 + + +def cumsum(l): + v = 0 + for i in l: + v += i + yield v + + +def naturalSortKey(key: Union[str, Path]): + if isinstance(key, Path): + key = key.name + try: + return int(key.rsplit(' ')[1]) # If the key ends in an integer, split that off and use that as the sort key. + except: + return key # Otherwise, just use the key. + + +if args.current_dir and len(args.tournament) == 0: + tournamentDirs = [Path('')] +else: + if len(args.tournament) == 0: + tournamentDirs = None + logsDir = Path(__file__).parent / "Logs" + if logsDir.exists(): + tournamentFolders = list(logsDir.resolve().glob("Tournament*")) + if len(tournamentFolders) > 0: + tournamentFolders = sorted(list(dir for dir in tournamentFolders if dir.is_dir()), key=naturalSortKey) + if len(tournamentFolders) > 0: + tournamentDirs = [tournamentFolders[-1]] # Latest tournament dir + if tournamentDirs is None: # Didn't find a tournament dir, revert to current-dir + tournamentDirs = [Path('')] + args.current_dir = True + else: + tournamentDirs = [Path(tournamentDir) for tournamentDir in args.tournament] # Specified tournament dir + + +for tournamentNumber, tournamentDir in enumerate(tournamentDirs): + try: + with open(tournamentDir / "summary.json", 'r') as f: + summary = json.load(f) + with open(tournamentDir / "results.json", 'r') as f: + results = json.load(f) + weights = {k: w for k, w in summary['meta']['score weights'].items() if w != 0} + pvp_data = {} + pvp_score = {'score weights': weights} + for stage_index, stage in results.items(): + pvp_data[stage_index] = {} + pvp_score[stage_index] = {} + for heat_index, heat in stage.items(): + pvp_data[stage_index][heat_index] = {} + number_of_opponents = len(heat['craft']) - 1 + for craft, stats in heat['craft'].items(): + if craft not in pvp_score[stage_index]: + pvp_score[stage_index][craft] = {} + pvp_data[stage_index][heat_index][craft] = {} + pvp_data[stage_index][heat_index][craft]['shared'] = { + 'wins': 1 if heat['result']['result'] == "Win" and craft in next(iter(heat['result']['teams'].values())).split(", ") else 0, + 'survivedCount': 1 if craft in heat['craft'] and stats['state'] == 'ALIVE' else 0, + 'miaCount': 1 if craft in heat['craft'] and stats['state'] == 'MIA' else 0, + 'deathCount': 1 if stats['state'] == 'DEAD' else 0, + 'deathOrder': stats['deathOrder'] / len(heat['craft']) if craft in heat['craft'] and 'deathOrder' in stats else 1, + 'deathTime': stats['deathTime'] if 'deathTime' in stats else heat['duration'], + 'HPremaining': CalculateAvgHP(stats['HPremaining'] if 'HPremaining' in stats and stats['state'] == 'ALIVE' else 0, 1 if stats['state'] == 'ALIVE' else 0), + 'accuracy': CalculateAccuracy(stats['hits'] if 'hits' in stats else 0, stats['shots'] if 'shots' in stats else 0), + 'rocket_accuracy': CalculateAccuracy(stats['rocket_strikes'] if 'rocket_strikes' in stats else 0, stats['rockets_fired'] if 'rockets_fired' in stats else 0), + } + score_vs_all = sum(weights[k] * pvp_data[stage_index][heat_index][craft]['shared'].get(k, 0) for k in weights) # To be shared amongst all opponents. + pvp_data[stage_index][heat_index][craft]['individual'] = {} + for opponent, data in heat['craft'].items(): + if opponent == craft: + continue + pvp_data[stage_index][heat_index][craft]['individual'][opponent] = { + 'cleanKills': 1 if any((field in data and data[field] == craft) for field in ('cleanKillBy', 'cleanRocketKillBy', 'cleanMissileKillBy', 'cleanRamKillBy')) else 0, + 'assists': 1 if data['state'] == 'DEAD' and any(field in data and craft in data[field] for field in ('hitsBy', 'rocketPartsHitBy', 'missilePartsHitBy', 'rammedPartsLostBy')) and not any((field in data) for field in ('cleanKillBy', 'cleanRocketKillBy', 'cleanMissileKillBy', 'cleanRamKillBy')) else 0, + 'hits': data['hitsBy'][craft] if 'hitsBy' in data and craft in data['hitsBy'] else 0, + 'hitsTaken': stats['hitsBy'][opponent] if 'hitsBy' in stats and opponent in stats['hitsBy'] else 0, + 'bulletDamage': data['bulletDamageBy'][craft] if 'bulletDamageBy' in data and craft in data['bulletDamageBy'] else 0, + 'bulletDamageTaken': stats['bulletDamageBy'][opponent] if 'bulletDamageBy' in stats and opponent in stats['bulletDamageBy'] else 0, + 'rocketHits': data['rocketHitsBy'][craft] if 'rocketHitsBy' in data and craft in data['rocketHitsBy'] else 0, + 'rocketHitsTaken': stats['rocketHitsBy'][opponent] if 'rocketHitsBy' in stats and opponent in stats['rocketHitsBy'] else 0, + 'rocketPartsHit': data['rocketPartsHitBy'][craft] if 'rocketPartsHitBy' in data and craft in data['rocketPartsHitBy'] else 0, + 'rocketPartsHitTaken': stats['rocketPartsHitBy'][opponent] if 'rocketPartsHitBy' in stats and opponent in stats['rocketPartsHitBy'] else 0, + 'rocketDamage': data['rocketDamageBy'][craft] if 'rocketDamageBy' in data and craft in data['rocketDamageBy'] else 0, + 'rocketDamageTaken': stats['rocketDamageBy'][opponent] if 'rocketDamageBy' in stats and opponent in stats['rocketDamageBy'] else 0, + 'missileHits': data['missileHitsBy'][craft] if 'missileHitsBy' in data and craft in data['missileHitsBy'] else 0, + 'missileHitsTaken': stats['missileHitsBy'][opponent] if 'missileHitsBy' in stats and opponent in stats['missileHitsBy'] else 0, + 'missilePartsHit': data['missilePartsHitBy'][craft] if 'missilePartsHitBy' in data and craft in data['missilePartsHitBy'] else 0, + 'missilePartsHitTaken': stats['missilePartsHitBy'][opponent] if 'missilePartsHitBy' in stats and opponent in stats['missilePartsHitBy'] else 0, + 'missileDamage': data['missileDamageBy'][craft] if 'missileDamageBy' in data and craft in data['missileDamageBy'] else 0, + 'missileDamageTaken': stats['missileDamageBy'][opponent] if 'missileDamageBy' in stats and opponent in stats['missileDamageBy'] else 0, + 'ramScore': data['rammedPartsLostBy'][craft] if 'rammedPartsLostBy' in data and craft in data['rammedPartsLostBy'] else 0, + 'ramScoreTaken': stats['rammedPartsLostBy'][opponent] if 'rammedPartsLostBy' in stats and opponent in stats['rammedPartsLostBy'] else 0, + 'battleDamage': data['battleDamageBy'][craft] if 'battleDamageBy' in data and craft in data['battleDamageBy'] else 0, + 'battleDamageTaken': stats['battleDamageBy'][opponent] if 'battleDamageBy' in stats and opponent in stats['battleDamageBy'] else 0, + } + score_vs_opponent = sum(weights[k] * pvp_data[stage_index][heat_index][craft]['individual'][opponent].get(k, 0) for k in weights) + pvp_score[stage_index][craft][opponent] = pvp_score[stage_index][craft].get(opponent, 0) + score_vs_opponent + score_vs_all / number_of_opponents + + # Add in a totals over the entire tournament entry. + players = list(set().union(*[set(round_data.keys()) for round_index, round_data in pvp_score.items() if round_index.startswith('Round')])) + score_totals = {player1: {player2: sum(round_data.get(player1, {}).get(player2, 0) for round_index, round_data in pvp_score.items() if round_index.startswith('Round')) for player2 in players} for player1 in players} # Combine scores over all rounds + players = sorted(players, key=lambda p: sum(score_totals[p].values()), reverse=True) # Sort by overall rank + pvp_score['totals'] = score_totals + + with open(tournamentDir / "pvp_scores.json", 'w') as f: + json.dump(pvp_score, f, indent=2) + + if args.csv: + lines = ['Player,' + ','.join(players) + ',Sum'] + [f'{player},' + ','.join(str(s) for s in score_totals[player].values()) + f",{sum(score_totals[player].values())}" for player in players] + with open(tournamentDir / "pvp_scores.csv", 'w') as f: + f.write('\n'.join(lines)) + + if args.plot: + grand_totals = {player: sum(scores.values()) for player, scores in score_totals.items()} + players = sorted(grand_totals, key=lambda k: grand_totals[k], reverse=True) + L = len(score_totals) + nodes = {players[i]:(math.sin(math.pi*2*i/L), math.cos(math.pi*2*i/L)) for i in range(L)} + edges = {p0:{p1:(nodes[p0], nodes[p1], score_totals[p0][p1]) for p1 in players} for p0 in players} + colourmap = plt.get_cmap('hsv') + colours = {players[i]: colourmap(i/L) for i in range(L)} + plt.figure(figsize=(16, 10), dpi=200) + lines = [] + for p0 in players: + for p1 in players: + if p0==p1:continue + lines.append(([edges[p0][p1][0][0], edges[p0][p1][1][0]], [edges[p0][p1][0][1], edges[p0][p1][1][1]], colours[p0], edges[p0][p1][2])) # x, y, colour, width + lines = sorted(lines, key=lambda l:l[3], reverse=True) + minWidth = min([line[3] for line in lines]) + maxWidth = max([line[3] for line in lines]) + for p in players: + plt.plot(nodes[p][0], nodes[p][1], color=colours[p], marker='*', markersize=20) + for line in lines: + plt.plot(line[0], line[1], color=line[2], linewidth=line[3]*10/(maxWidth-minWidth)+minWidth+2, solid_capstyle='round') + plt.legend(players, loc='upper right') + plt.axis('equal') + plt.show() + + except Exception as e: + print(f"Failed to parse {tournamentDir}. Have you run the tournament parser on it first?") + traceback.print_exc() + continue diff --git a/BDArmory/Distribution/GameData/BDArmory/parse_tournament_log_files.py b/BDArmory/Distribution/GameData/BDArmory/parse_tournament_log_files.py index 4f7b682e0..9d843de8e 100755 --- a/BDArmory/Distribution/GameData/BDArmory/parse_tournament_log_files.py +++ b/BDArmory/Distribution/GameData/BDArmory/parse_tournament_log_files.py @@ -11,7 +11,7 @@ from pathlib import Path from typing import Dict, List, Tuple, Union -VERSION = "1.15.0" +VERSION = "1.22.0" parser = argparse.ArgumentParser(description="Tournament log parser", formatter_class=argparse.ArgumentDefaultsHelpFormatter) parser.add_argument('tournament', type=str, nargs='*', help="Tournament folder to parse.") @@ -19,14 +19,14 @@ parser.add_argument('-n', '--no-files', action='store_true', help="Don't create summary files.") parser.add_argument('-s', '--score', action='store_false', help="Compute scores.") parser.add_argument('-so', '--scores-only', action='store_true', help="Only display the scores in the summary on the console.") -parser.add_argument('-w', '--weights', type=str, default="1,0,0,-1,1,2e-3,3,1.5,4e-3,0,1e-4,4e-5,0.035,0,6e-4,0,1.5e-4,5e-5,0.15,0,0.002,0,3e-5,1.5e-5,0.075,0,0,0,0,0,10,-1,-1", help="Score weights (in order of main columns from 'Wins' to 'Ram', plus others). Use --show-weights to see them.") -# Note: in non-ranked FFA tournaments, a 'Wins' weight of 1 is good, but in ranked rounds this pushes the top ranked plane's score extra high, so a lower value should be used, e.g., 0.25. -# Old weights: 0.1,0,0,-1,1,2e-3,2,1,2e-2,0,2e-4,1e-4,0.05,0,2e-3,0,1e-4,5e-5,0.5,0,0.01,0,2e-5,1e-5,0.01,0,0,0,0,0 +parser.add_argument('-w', '--weights', type=str, default="1,0,0,-1,1,2e-3,3,1.5,4e-3,0,1e-4,4e-5,0.035,0,6e-4,0,1.5e-4,5e-5,0.15,0,0.002,0,3e-5,1.5e-5,0.075,0,0,0,0,0,0,10,-1,-1", help="Score weights (in order of main columns from 'Wins' to 'Ram', plus others). Use --show-weights to see them.") parser.add_argument('-c', '--current-dir', action='store_true', help="Parse the logs in the current directory as if it was a tournament without the folder structure.") parser.add_argument('-nc', '--no-cumulative', action='store_true', help="Don't display cumulative scores at the end.") +parser.add_argument('-nh', '--no-header', action='store_true', help="Don't display the header.") parser.add_argument('-N', type=int, help="Only the first N logs in the folder (in -c mode).") parser.add_argument('-z', '--zero-lowest-score', action='store_true', help="Shift the scores so that the lowest is 0.") parser.add_argument('-sw', '--show-weights', action='store_true', help="Display the score weights.") +parser.add_argument('-wp', '--waypoint-scores', action='store_true', help="Use the default waypoint scores.") parser.add_argument("--version", action='store_true', help="Show the script version, then exit.") args = parser.parse_args() args.score = args.score or args.scores_only @@ -35,6 +35,16 @@ print(f"Version: {VERSION}") sys.exit() + +def naturalSortKey(key: Union[str, Path]): + if isinstance(key, Path): + key = key.name + try: + return int(key.rsplit(' ')[1]) # If the key ends in an integer, split that off and use that as the sort key. + except: + return key # Otherwise, just use the key. + + if args.current_dir and len(args.tournament) == 0: tournamentDirs = [Path('')] else: @@ -44,7 +54,7 @@ if logsDir.exists(): tournamentFolders = list(logsDir.resolve().glob("Tournament*")) if len(tournamentFolders) > 0: - tournamentFolders = sorted(list(dir for dir in tournamentFolders if dir.is_dir())) + tournamentFolders = sorted(list(dir for dir in tournamentFolders if dir.is_dir()), key=naturalSortKey) if len(tournamentFolders) > 0: tournamentDirs = [tournamentFolders[-1]] # Latest tournament dir if tournamentDirs is None: # Didn't find a tournament dir, revert to current-dir @@ -53,12 +63,14 @@ else: tournamentDirs = [Path(tournamentDir) for tournamentDir in args.tournament] # Specified tournament dir -if args.score: - score_fields = ('wins', 'survivedCount', 'miaCount', 'deathCount', 'deathOrder', 'deathTime', 'cleanKills', 'assists', 'hits', 'hitsTaken', 'bulletDamage', 'bulletDamageTaken', 'rocketHits', 'rocketHitsTaken', 'rocketPartsHit', 'rocketPartsHitTaken', 'rocketDamage', 'rocketDamageTaken', 'missileHits', 'missileHitsTaken', 'missilePartsHit', 'missilePartsHitTaken', 'missileDamage', 'missileDamageTaken', 'ramScore', 'ramScoreTaken', 'battleDamage', 'HPremaining', 'accuracy', 'rocket_accuracy', 'waypointCount', 'waypointTime', 'waypointDeviation') - try: - weights = list(float(w) for w in args.weights.split(',')) - except: - weights = [] +if args.waypoint_scores: + args.weights = "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,-0.02,-0.003" + +score_fields = ('wins', 'survivedCount', 'miaCount', 'deathCount', 'deathOrder', 'deathTime', 'cleanKills', 'assists', 'hits', 'hitsTaken', 'bulletDamage', 'bulletDamageTaken', 'rocketHits', 'rocketHitsTaken', 'rocketPartsHit', 'rocketPartsHitTaken', 'rocketDamage', 'rocketDamageTaken', 'missileHits', 'missileHitsTaken', 'missilePartsHit', 'missilePartsHitTaken', 'missileDamage', 'missileDamageTaken', 'ramScore', 'ramScoreTaken', 'battleDamage', 'partsLostToAsteroids', 'HPremaining', 'accuracy', 'rocket_accuracy', 'waypointCount', 'waypointTime', 'waypointDeviation') +try: + weights = list(float(w) for w in args.weights.split(',')) +except: + weights = [] if args.show_weights: field_width = max(len(f) for f in score_fields) @@ -80,15 +92,6 @@ def cumsum(l): yield v -def naturalSortKey(key: Union[str, Path]): - if isinstance(key, Path): - key = key.name - try: - return int(key.rsplit(' ')[1]) # If the key ends in an integer, split that off and use that as the sort key. - except: - return key # Otherwise, just use the key. - - def encode_names(log_lines: List[str]) -> Tuple[Dict[str, str], List[str]]: """ Encode the craft names in base64 to avoid issues with naming. @@ -116,7 +119,7 @@ def encode_names(log_lines: List[str]) -> Tuple[Dict[str, str], List[str]]: craft_names.update({json.dumps(name)[1:-1] for name in craft_names}) craft_names = {cn: b64encode(cn.encode()) for cn in craft_names} sorted_craft_names = list(sorted(craft_names, key=lambda k: len(k), reverse=True)) # Sort the craft names from longest to shortest to avoid accidentally replacing substrings. - for i in range(len(log_lines)): + for i in range(1, len(log_lines)): # The first line doesn't contain craft names for name in sorted_craft_names: log_lines[i] = log_lines[i].replace(name, craft_names[name].decode()) encoded_craft_names = {v.decode(): k for k, v in craft_names.items()} @@ -128,9 +131,10 @@ def encode_names(log_lines: List[str]) -> Tuple[Dict[str, str], List[str]]: print("") tournamentData = {} tournamentMetadata = {} - m = re.search('Tournament (\d+)', str(tournamentDir)) + m = re.search('Tournament (\\d+)', str(tournamentDir)) if m is not None and len(m.groups()) > 0: tournamentMetadata['ID'] = m.groups()[0] + tournamentMetadata['rounds'] = len([roundDir for roundDir in tournamentDir.iterdir() if roundDir.is_dir() and roundDir.name.startswith('Round')]) for round in sorted((roundDir for roundDir in tournamentDir.iterdir() if roundDir.is_dir()), key=naturalSortKey) if not args.current_dir else (tournamentDir,): if not args.current_dir and len(round.name) == 0: continue @@ -140,7 +144,7 @@ def encode_names(log_lines: List[str]) -> Tuple[Dict[str, str], List[str]]: del tournamentData[round.name] continue for heat in logFiles if args.N == None else logFiles[:args.N]: - with open(heat, "r") as logFile: + with open(heat, "r", encoding="utf-8") as logFile: log_lines = [line.strip() for line in logFile] tournamentData[round.name][heat.name] = {'result': None, 'duration': 0, 'craft': {}} encoded_craft_names, log_lines = encode_names(log_lines) @@ -241,6 +245,9 @@ def encode_names(log_lines: List[str]) -> Tuple[Dict[str, str], List[str]]: elif field.startswith('GMKILL'): _, craft, reason = field.split(':', 2) tournamentData[round.name][heat.name]['craft'][encoded_craft_names[craft]].update({'GMKillReason': reason}) + elif field.startswith('PARTSLOSTTOASTEROIDS:'): + _, craft, partsLost = field.split(':', 2) + tournamentData[round.name][heat.name]['craft'][encoded_craft_names[craft]].update({'partsLostToAsteroids': int(partsLost)}) elif field.startswith('HPLEFT:'): _, craft, hp = field.split(':', 2) tournamentData[round.name][heat.name]['craft'][encoded_craft_names[craft]].update({'HPremaining': float(hp)}) @@ -257,22 +264,22 @@ def encode_names(log_lines: List[str]) -> Tuple[Dict[str, str], List[str]]: if (len(heat_result) > 2): teams = json.loads(heat_result[2]) if isinstance(teams, dict): # Win, single team - tournamentData[round.name][heat.name]['result'] = {'result': result_type, 'teams': {teams['team']: ', '.join(teams['members'])}} + tournamentData[round.name][heat.name]['result'] = {'result': result_type, 'teams': {encoded_craft_names.get(teams['team'], teams['team']): ', '.join((encoded_craft_names[craft] for craft in teams['members']))}} elif isinstance(teams, list): # Draw, multiple teams - tournamentData[round.name][heat.name]['result'] = {'result': result_type, 'teams': {team['team']: ', '.join(team['members']) for team in teams}} + tournamentData[round.name][heat.name]['result'] = {'result': result_type, 'teams': {encoded_craft_names.get(team['team'], team['team']): ', '.join((encoded_craft_names[craft] for craft in team['members'])) for team in teams}} else: # Mutual Annihilation tournamentData[round.name][heat.name]['result'] = {'result': result_type} elif field.startswith('DEADTEAMS:'): dead_teams = json.loads(field.split(':', 1)[1]) if len(dead_teams) > 0: - tournamentData[round.name][heat.name]['result'].update({'dead teams': {team['team']: ', '.join(team['members']) for team in dead_teams}}) + tournamentData[round.name][heat.name]['result'].update({'dead teams': {encoded_craft_names.get(team['team'], team['team']): ', '.join((encoded_craft_names[craft] for craft in team['members'])) for team in dead_teams}}) # Ignore Tag mode for now. elif field.startswith('WAYPOINTS:'): _, craft, waypoints_str = field.split(':', 2) tournamentData[round.name][heat.name]['craft'][encoded_craft_names[craft]].update({'waypoints': [waypoint.split(':') for waypoint in waypoints_str.split(';')]}) # List[Tuple[int, float, float]] = [(index, deviation, timestamp),] if not args.no_files and len(tournamentData) > 0: - with open(tournamentDir / 'results.json', 'w') as outFile: + with open(tournamentDir / 'results.json', 'w', encoding="utf-8") as outFile: json.dump(tournamentData, outFile, indent=2) craftNames = sorted(list(set(craft for round in tournamentData.values() for heat in round.values() for craft in heat['craft'].keys()))) @@ -285,6 +292,8 @@ def encode_names(log_lines: List[str]) -> Tuple[Dict[str, str], List[str]]: 'meta': { 'ID': tournamentMetadata.get('ID', 'unknown'), 'duration': [ts.isoformat() for ts in tournamentMetadata.get('duration', (datetime.now(), datetime.now()))], + 'rounds': tournamentMetadata.get('rounds', -1), + 'score weights': {f: w for f, w in zip(score_fields, weights)}, }, 'craft': { craft: { @@ -330,12 +339,10 @@ def encode_names(log_lines: List[str]) -> Tuple[Dict[str, str], List[str]]: 'ramScoreTaken': sum([sum(heat['craft'][craft]['rammedPartsLostBy'].values()) for round in tournamentData.values() for heat in round.values() if craft in heat['craft'] and 'rammedPartsLostBy' in heat['craft'][craft]]), 'battleDamage': sum([data[field][craft] for round in tournamentData.values() for heat in round.values() for player, data in heat['craft'].items() if player != craft for field in ('battleDamageBy',) if field in data and craft in data[field]]), 'battleDamageTaken': sum([sum(heat['craft'][craft]['battleDamageBy'].values()) for round in tournamentData.values() for heat in round.values() if craft in heat['craft'] and 'battleDamageBy' in heat['craft'][craft]]), + 'partsLostToAsteroids': sum([heat['craft'][craft]['partsLostToAsteroids'] for round in tournamentData.values() for heat in round.values() if craft in heat['craft'] and 'partsLostToAsteroids' in heat['craft'][craft]]), 'HPremaining': CalculateAvgHP(sum([heat['craft'][craft]['HPremaining'] for round in tournamentData.values() for heat in round.values() if craft in heat['craft'] and 'HPremaining' in heat['craft'][craft] and heat['craft'][craft]['state'] == 'ALIVE']), len([1 for round in tournamentData.values() for heat in round.values() if craft in heat['craft'] and heat['craft'][craft]['state'] == 'ALIVE'])), 'accuracy': CalculateAccuracy(sum([heat['craft'][craft]['hits'] for round in tournamentData.values() for heat in round.values() if craft in heat['craft'] and 'hits' in heat['craft'][craft]]), sum([heat['craft'][craft]['shots'] for round in tournamentData.values() for heat in round.values() if craft in heat['craft'] and 'shots' in heat['craft'][craft]])), 'rocket_accuracy': CalculateAccuracy(sum([heat['craft'][craft]['rocket_strikes'] for round in tournamentData.values() for heat in round.values() if craft in heat['craft'] and 'rocket_strikes' in heat['craft'][craft]]), sum([heat['craft'][craft]['rockets_fired'] for round in tournamentData.values() for heat in round.values() if craft in heat['craft'] and 'rockets_fired' in heat['craft'][craft]])), - 'waypointCount': sum(len(heat['craft'][craft]['waypoints']) for round in tournamentData.values() for heat in round.values() if craft in heat['craft'] and 'waypoints' in heat['craft'][craft]), - 'waypointTime': sum((float(heat['craft'][craft]['waypoints'][-1][2]) - float(heat['craft'][craft]['waypoints'][0][2])) for round in tournamentData.values() for heat in round.values() if craft in heat['craft'] and 'waypoints' in heat['craft'][craft]), - 'waypointDeviation': sum(sum(float(waypoint[1]) for waypoint in heat['craft'][craft]['waypoints']) for round in tournamentData.values() for heat in round.values() if craft in heat['craft'] and 'waypoints' in heat['craft'][craft]), } for craft in craftNames }, @@ -355,16 +362,91 @@ def encode_names(log_lines: List[str]) -> Tuple[Dict[str, str], List[str]]: 'damage/spawn': craft['bulletDamage'] / spawns if spawns > 0 else 0, }) + per_round_summary = { # Compute this here, since we need the per-round waypoint info to avoid negative scores. + craft: [ + { + 'wins': len([1 for heat in round.values() if heat['result']['result'] == "Win" and craft in next(iter(heat['result']['teams'].values())).split(", ")]), + 'survivedCount': len([1 for heat in round.values() if craft in heat['craft'] and heat['craft'][craft]['state'] == 'ALIVE']), + 'miaCount': len([1 for heat in round.values() if craft in heat['craft'] and heat['craft'][craft]['state'] == 'MIA']), + 'deathCount': ( + len([1 for heat in round.values() if craft in heat['craft'] and heat['craft'][craft]['state'] == 'DEAD']), # Total + len([1 for heat in round.values() if craft in heat['craft'] and heat['craft'][craft]['state'] == 'DEAD' and 'cleanKillBy' in heat['craft'][craft]]), # Bullets + len([1 for heat in round.values() if craft in heat['craft'] and heat['craft'][craft]['state'] == 'DEAD' and 'cleanRocketKillBy' in heat['craft'][craft]]), # Rockets + len([1 for heat in round.values() if craft in heat['craft'] and heat['craft'][craft]['state'] == 'DEAD' and 'cleanMissileKillBy' in heat['craft'][craft]]), # Missiles + len([1 for heat in round.values() if craft in heat['craft'] and heat['craft'][craft]['state'] == 'DEAD' and 'cleanRamKillBy' in heat['craft'][craft]]), # Rams + len([1 for heat in round.values() if craft in heat['craft'] and heat['craft'][craft]['state'] == 'DEAD' and not any(field in heat['craft'][craft] for field in ('cleanKillBy', 'cleanRocketKillBy', 'cleanMissileKillBy', 'cleanRamKillBy')) and any(field in heat['craft'][craft] for field in ('hitsBy', 'rocketPartsHitBy', 'missilePartsHitBy', 'rammedPartsLostBy'))]), # Dirty kill + len([1 for heat in round.values() if craft in heat['craft'] and heat['craft'][craft]['state'] == 'DEAD' and not any(field in heat['craft'][craft] for field in ('hitsBy', 'rocketPartsHitBy', 'missilePartsHitBy', 'rammedPartsLostBy')) and not any('rammedPartsLostBy' in data and craft in data['rammedPartsLostBy'] for data in heat['craft'].values())]), # Suicide (died without being hit or ramming anyone). + ), + 'deathOrder': sum([heat['craft'][craft]['deathOrder'] / len(heat['craft']) if 'deathOrder' in heat['craft'][craft] else 1 for heat in round.values() if craft in heat['craft']]), + 'deathTime': sum([heat['craft'][craft]['deathTime'] if 'deathTime' in heat['craft'][craft] else heat['duration'] for heat in round.values() if craft in heat['craft']]), + 'cleanKills': ( + len([1 for heat in round.values() for data in heat['craft'].values() if any((field in data and data[field] == craft) for field in ('cleanKillBy', 'cleanRocketKillBy', 'cleanMissileKillBy', 'cleanRamKillBy'))]), # Total + len([1 for heat in round.values() for data in heat['craft'].values() if 'cleanKillBy' in data and data['cleanKillBy'] == craft]), # Bullets + len([1 for heat in round.values() for data in heat['craft'].values() if 'cleanRocketKillBy' in data and data['cleanRocketKillBy'] == craft]), # Rockets + len([1 for heat in round.values() for data in heat['craft'].values() if 'cleanMissileKillBy' in data and data['cleanMissileKillBy'] == craft]), # Missiles + len([1 for heat in round.values() for data in heat['craft'].values() if 'cleanRamKillBy' in data and data['cleanRamKillBy'] == craft]), # Rams + ), + 'assists': len([1 for heat in round.values() for data in heat['craft'].values() if data['state'] == 'DEAD' and any(field in data and craft in data[field] for field in ('hitsBy', 'rocketPartsHitBy', 'missilePartsHitBy', 'rammedPartsLostBy')) and not any((field in data) for field in ('cleanKillBy', 'cleanRocketKillBy', 'cleanMissileKillBy', 'cleanRamKillBy'))]), + 'hits': sum([heat['craft'][craft]['hits'] for heat in round.values() if craft in heat['craft'] and 'hits' in heat['craft'][craft]]), + 'hitsTaken': sum([sum(heat['craft'][craft]['hitsBy'].values()) for heat in round.values() if craft in heat['craft'] and 'hitsBy' in heat['craft'][craft]]), + 'bulletDamage': sum([data[field][craft] for heat in round.values() for data in heat['craft'].values() for field in ('bulletDamageBy',) if field in data and craft in data[field]]), + 'bulletDamageTaken': sum([sum(heat['craft'][craft]['bulletDamageBy'].values()) for heat in round.values() if craft in heat['craft'] and 'bulletDamageBy' in heat['craft'][craft]]), + 'rocketHits': sum([data[field][craft] for heat in round.values() for data in heat['craft'].values() for field in ('rocketHitsBy',) if field in data and craft in data[field]]), + 'rocketHitsTaken': sum([sum(heat['craft'][craft]['rocketHitsBy'].values()) for heat in round.values() if craft in heat['craft'] and 'rocketHitsBy' in heat['craft'][craft]]), + 'rocketPartsHit': sum([data[field][craft] for heat in round.values() for data in heat['craft'].values() for field in ('rocketPartsHitBy',) if field in data and craft in data[field]]), + 'rocketPartsHitTaken': sum([sum(heat['craft'][craft]['rocketPartsHitBy'].values()) for heat in round.values() if craft in heat['craft'] and 'rocketPartsHitBy' in heat['craft'][craft]]), + 'rocketDamage': sum([data[field][craft] for heat in round.values() for data in heat['craft'].values() for field in ('rocketDamageBy',) if field in data and craft in data[field]]), + 'rocketDamageTaken': sum([sum(heat['craft'][craft]['rocketDamageBy'].values()) for heat in round.values() if craft in heat['craft'] and 'rocketDamageBy' in heat['craft'][craft]]), + 'missileHits': sum([data[field][craft] for heat in round.values() for data in heat['craft'].values() for field in ('missileHitsBy',) if field in data and craft in data[field]]), + 'missileHitsTaken': sum([sum(heat['craft'][craft]['missileHitsBy'].values()) for heat in round.values() if craft in heat['craft'] and 'missileHitsBy' in heat['craft'][craft]]), + 'missilePartsHit': sum([data[field][craft] for heat in round.values() for data in heat['craft'].values() for field in ('missilePartsHitBy',) if field in data and craft in data[field]]), + 'missilePartsHitTaken': sum([sum(heat['craft'][craft]['missilePartsHitBy'].values()) for heat in round.values() if craft in heat['craft'] and 'missilePartsHitBy' in heat['craft'][craft]]), + 'missileDamage': sum([data[field][craft] for heat in round.values() for data in heat['craft'].values() for field in ('missileDamageBy',) if field in data and craft in data[field]]), + 'missileDamageTaken': sum([sum(heat['craft'][craft]['missileDamageBy'].values()) for heat in round.values() if craft in heat['craft'] and 'missileDamageBy' in heat['craft'][craft]]), + 'ramScore': sum([data[field][craft] for heat in round.values() for data in heat['craft'].values() for field in ('rammedPartsLostBy',) if field in data and craft in data[field]]), + 'ramScoreTaken': sum([sum(heat['craft'][craft]['rammedPartsLostBy'].values()) for heat in round.values() if craft in heat['craft'] and 'rammedPartsLostBy' in heat['craft'][craft]]), + 'battleDamage': sum([data[field][craft] for heat in round.values() for player, data in heat['craft'].items() if player != craft for field in ('battleDamageBy',) if field in data and craft in data[field]]), + 'battleDamageTaken': sum([sum(heat['craft'][craft]['battleDamageBy'].values()) for heat in round.values() if craft in heat['craft'] and 'battleDamageBy' in heat['craft'][craft]]), + 'partsLostToAsteroids': sum([heat['craft'][craft]['partsLostToAsteroids'] for heat in round.values() if craft in heat['craft'] and 'partsLostToAsteroids' in heat['craft'][craft]]), + 'HPremaining': CalculateAvgHP(sum([heat['craft'][craft]['HPremaining'] for heat in round.values() if craft in heat['craft'] and 'HPremaining' in heat['craft'][craft] and heat['craft'][craft]['state'] == 'ALIVE']), len([1 for heat in round.values() if craft in heat['craft'] and heat['craft'][craft]['state'] == 'ALIVE'])), + 'accuracy': CalculateAccuracy(sum([heat['craft'][craft]['hits'] for heat in round.values() if craft in heat['craft'] and 'hits' in heat['craft'][craft]]), sum([heat['craft'][craft]['shots'] for heat in round.values() if craft in heat['craft'] and 'shots' in heat['craft'][craft]])), + 'rocket_accuracy': CalculateAccuracy(sum([heat['craft'][craft]['rocket_strikes'] for heat in round.values() if craft in heat['craft'] and 'rocket_strikes' in heat['craft'][craft]]), sum([heat['craft'][craft]['rockets_fired'] for heat in round.values() if craft in heat['craft'] and 'rockets_fired' in heat['craft'][craft]])), + 'waypointCount': sum(len(heat['craft'][craft]['waypoints']) for heat in round.values() if craft in heat['craft'] and 'waypoints' in heat['craft'][craft]), + 'waypointTime': sum(float(heat['craft'][craft]['waypoints'][-1][2]) - float(heat['craft'][craft]['waypoints'][0][2]) for heat in round.values() if craft in heat['craft'] and 'waypoints' in heat['craft'][craft]), + 'waypointDeviation': sum(float(waypoint[1]) for heat in round.values() if craft in heat['craft'] and 'waypoints' in heat['craft'][craft] for waypoint in heat['craft'][craft]['waypoints']), + } for round in tournamentData.values() + ] for craft in craftNames + } + + hasWaypoints = False + if any('waypoints' in heat['craft'][craft].keys() for round in tournamentData.values() for heat in round.values() for craft in craftNames if craft in heat['craft']): + hasWaypoints = True + for craft in craftNames: + WPbestCount = max((len(heat['craft'][craft]['waypoints']) for round in tournamentData.values() for heat in round.values() if craft in heat['craft'] and 'waypoints' in heat['craft'][craft]), default=0) + summary['craft'][craft].update({ + 'waypointCount': sum(len(heat['craft'][craft]['waypoints']) for round in tournamentData.values() for heat in round.values() if craft in heat['craft'] and 'waypoints' in heat['craft'][craft]), + 'waypointTime': sum((float(heat['craft'][craft]['waypoints'][-1][2]) - float(heat['craft'][craft]['waypoints'][0][2])) for round in tournamentData.values() for heat in round.values() if craft in heat['craft'] and 'waypoints' in heat['craft'][craft]), + 'waypointDeviation': sum(sum(float(waypoint[1]) for waypoint in heat['craft'][craft]['waypoints']) for round in tournamentData.values() for heat in round.values() if craft in heat['craft'] and 'waypoints' in heat['craft'][craft]), + 'waypointBestCount': WPbestCount, + 'waypointBestTime': min(((float(heat['craft'][craft]['waypoints'][-1][2]) - float(heat['craft'][craft]['waypoints'][0][2])) for round in tournamentData.values() for heat in round.values() if craft in heat['craft'] and 'waypoints' in heat['craft'][craft] and len(heat['craft'][craft]['waypoints']) == WPbestCount), default=0), + 'waypointBestDeviation': min((sum(float(waypoint[1]) for waypoint in heat['craft'][craft]['waypoints']) for round in tournamentData.values() for heat in round.values() if craft in heat['craft'] and 'waypoints' in heat['craft'][craft] and len(heat['craft'][craft]['waypoints']) == WPbestCount), default=0), + }) + if args.score: - for craft in summary['craft'].values(): - craft.update({'score': sum(w * craft[f][0] if isinstance(craft[f], tuple) else w * craft[f] for w, f in zip(weights, score_fields))}) + for craftName, summary_data in summary['craft'].items(): + score = sum(w * summary_data[f][0] if isinstance(summary_data[f], tuple) else w * summary_data[f] for w, f in zip(weights, score_fields) if f in summary_data and not f.startswith('waypoint')) # Treat waypoints separately so we can avoid non-negative scores for waypoints. + waypoint_data = per_round_summary[craftName] + score += sum(max(0, sum( + w * waypoint_data[round][f][0] if isinstance(waypoint_data[round][f], tuple) else w * waypoint_data[round][f] for w, f in zip(weights, score_fields) if f.startswith('waypoint') + )) for round in range(len(waypoint_data))) + summary_data.update({'score': score}) if args.zero_lowest_score and len(summary['craft']) > 0: - offset = min(craft['score'] for craft in summary['craft'].values()) - for craft in summary['craft'].values(): - craft['score'] -= offset + offset = min(summary_data['score'] for summary_data in summary['craft'].values()) + for summary_data in summary['craft'].values(): + summary_data['score'] -= offset if not args.no_files and len(summary['craft']) > 0: - with open(tournamentDir / 'summary.json', 'w') as outFile: + with open(tournamentDir / 'summary.json', 'w', encoding="utf-8") as outFile: json.dump(summary, outFile, indent=2) if len(summary['craft']) > 0: @@ -373,7 +455,6 @@ def encode_names(log_lines: List[str]) -> Tuple[Dict[str, str], List[str]]: csv_summary = ["craft," + ",".join( ",".join(('deathCount', 'dcB', 'dcR', 'dcM', 'dcR', 'dcA', 'dcS')) if k == 'deathCount' else ",".join(('cleanKills', 'ckB', 'ckR', 'ckM', 'ckR')) if k == 'cleanKills' else - ",".join(('wpCount', 'wpDeviation', 'wpTime')) if k == 'waypoints' else k for k in headers), ] for craft, score in sorted(summary['craft'].items(), key=lambda i: i[1]['score'], reverse=True): csv_summary.append(craft + "," + ",".join( @@ -382,80 +463,34 @@ def encode_names(log_lines: List[str]) -> Tuple[Dict[str, str], List[str]]: else str(int(100 * score[h]) / 100) for h in headers)) # Write main summary results to the summary.csv file. - with open(tournamentDir / 'summary.csv', 'w') as outFile: + with open(tournamentDir / 'summary.csv', 'w', encoding="utf-8") as outFile: outFile.write("\n".join(csv_summary)) teamNames = sorted(list(set([team for result_type in summary['team results'].values() for team in result_type]))) default_team_names = [chr(k) for k in range(ord('A'), ord('A') + len(summary['craft']))] if args.score and not args.no_cumulative: # Per round scores. - per_round_summary = { - craft: [ - { - 'wins': len([1 for heat in round.values() if heat['result']['result'] == "Win" and craft in next(iter(heat['result']['teams'].values())).split(", ")]), - 'survivedCount': len([1 for heat in round.values() if craft in heat['craft'] and heat['craft'][craft]['state'] == 'ALIVE']), - 'miaCount': len([1 for heat in round.values() if craft in heat['craft'] and heat['craft'][craft]['state'] == 'MIA']), - 'deathCount': ( - len([1 for heat in round.values() if craft in heat['craft'] and heat['craft'][craft]['state'] == 'DEAD']), # Total - len([1 for heat in round.values() if craft in heat['craft'] and heat['craft'][craft]['state'] == 'DEAD' and 'cleanKillBy' in heat['craft'][craft]]), # Bullets - len([1 for heat in round.values() if craft in heat['craft'] and heat['craft'][craft]['state'] == 'DEAD' and 'cleanRocketKillBy' in heat['craft'][craft]]), # Rockets - len([1 for heat in round.values() if craft in heat['craft'] and heat['craft'][craft]['state'] == 'DEAD' and 'cleanMissileKillBy' in heat['craft'][craft]]), # Missiles - len([1 for heat in round.values() if craft in heat['craft'] and heat['craft'][craft]['state'] == 'DEAD' and 'cleanRamKillBy' in heat['craft'][craft]]), # Rams - len([1 for heat in round.values() if craft in heat['craft'] and heat['craft'][craft]['state'] == 'DEAD' and not any(field in heat['craft'][craft] for field in ('cleanKillBy', 'cleanRocketKillBy', 'cleanMissileKillBy', 'cleanRamKillBy')) and any(field in heat['craft'][craft] for field in ('hitsBy', 'rocketPartsHitBy', 'missilePartsHitBy', 'rammedPartsLostBy'))]), # Dirty kill - len([1 for heat in round.values() if craft in heat['craft'] and heat['craft'][craft]['state'] == 'DEAD' and not any(field in heat['craft'][craft] for field in ('hitsBy', 'rocketPartsHitBy', 'missilePartsHitBy', 'rammedPartsLostBy')) and not any('rammedPartsLostBy' in data and craft in data['rammedPartsLostBy'] for data in heat['craft'].values())]), # Suicide (died without being hit or ramming anyone). - ), - 'deathOrder': sum([heat['craft'][craft]['deathOrder'] / len(heat['craft']) if 'deathOrder' in heat['craft'][craft] else 1 for heat in round.values() if craft in heat['craft']]), - 'deathTime': sum([heat['craft'][craft]['deathTime'] if 'deathTime' in heat['craft'][craft] else heat['duration'] for heat in round.values() if craft in heat['craft']]), - 'cleanKills': ( - len([1 for heat in round.values() for data in heat['craft'].values() if any((field in data and data[field] == craft) for field in ('cleanKillBy', 'cleanRocketKillBy', 'cleanMissileKillBy', 'cleanRamKillBy'))]), # Total - len([1 for heat in round.values() for data in heat['craft'].values() if 'cleanKillBy' in data and data['cleanKillBy'] == craft]), # Bullets - len([1 for heat in round.values() for data in heat['craft'].values() if 'cleanRocketKillBy' in data and data['cleanRocketKillBy'] == craft]), # Rockets - len([1 for heat in round.values() for data in heat['craft'].values() if 'cleanMissileKillBy' in data and data['cleanMissileKillBy'] == craft]), # Missiles - len([1 for heat in round.values() for data in heat['craft'].values() if 'cleanRamKillBy' in data and data['cleanRamKillBy'] == craft]), # Rams - ), - 'assists': len([1 for heat in round.values() for data in heat['craft'].values() if data['state'] == 'DEAD' and any(field in data and craft in data[field] for field in ('hitsBy', 'rocketPartsHitBy', 'missilePartsHitBy', 'rammedPartsLostBy')) and not any((field in data) for field in ('cleanKillBy', 'cleanRocketKillBy', 'cleanMissileKillBy', 'cleanRamKillBy'))]), - 'hits': sum([heat['craft'][craft]['hits'] for heat in round.values() if craft in heat['craft'] and 'hits' in heat['craft'][craft]]), - 'hitsTaken': sum([sum(heat['craft'][craft]['hitsBy'].values()) for heat in round.values() if craft in heat['craft'] and 'hitsBy' in heat['craft'][craft]]), - 'bulletDamage': sum([data[field][craft] for heat in round.values() for data in heat['craft'].values() for field in ('bulletDamageBy',) if field in data and craft in data[field]]), - 'bulletDamageTaken': sum([sum(heat['craft'][craft]['bulletDamageBy'].values()) for heat in round.values() if craft in heat['craft'] and 'bulletDamageBy' in heat['craft'][craft]]), - 'rocketHits': sum([data[field][craft] for heat in round.values() for data in heat['craft'].values() for field in ('rocketHitsBy',) if field in data and craft in data[field]]), - 'rocketHitsTaken': sum([sum(heat['craft'][craft]['rocketHitsBy'].values()) for heat in round.values() if craft in heat['craft'] and 'rocketHitsBy' in heat['craft'][craft]]), - 'rocketPartsHit': sum([data[field][craft] for heat in round.values() for data in heat['craft'].values() for field in ('rocketPartsHitBy',) if field in data and craft in data[field]]), - 'rocketPartsHitTaken': sum([sum(heat['craft'][craft]['rocketPartsHitBy'].values()) for heat in round.values() if craft in heat['craft'] and 'rocketPartsHitBy' in heat['craft'][craft]]), - 'rocketDamage': sum([data[field][craft] for heat in round.values() for data in heat['craft'].values() for field in ('rocketDamageBy',) if field in data and craft in data[field]]), - 'rocketDamageTaken': sum([sum(heat['craft'][craft]['rocketDamageBy'].values()) for heat in round.values() if craft in heat['craft'] and 'rocketDamageBy' in heat['craft'][craft]]), - 'missileHits': sum([data[field][craft] for heat in round.values() for data in heat['craft'].values() for field in ('missileHitsBy',) if field in data and craft in data[field]]), - 'missileHitsTaken': sum([sum(heat['craft'][craft]['missileHitsBy'].values()) for heat in round.values() if craft in heat['craft'] and 'missileHitsBy' in heat['craft'][craft]]), - 'missilePartsHit': sum([data[field][craft] for heat in round.values() for data in heat['craft'].values() for field in ('missilePartsHitBy',) if field in data and craft in data[field]]), - 'missilePartsHitTaken': sum([sum(heat['craft'][craft]['missilePartsHitBy'].values()) for heat in round.values() if craft in heat['craft'] and 'missilePartsHitBy' in heat['craft'][craft]]), - 'missileDamage': sum([data[field][craft] for heat in round.values() for data in heat['craft'].values() for field in ('missileDamageBy',) if field in data and craft in data[field]]), - 'missileDamageTaken': sum([sum(heat['craft'][craft]['missileDamageBy'].values()) for heat in round.values() if craft in heat['craft'] and 'missileDamageBy' in heat['craft'][craft]]), - 'ramScore': sum([data[field][craft] for heat in round.values() for data in heat['craft'].values() for field in ('rammedPartsLostBy',) if field in data and craft in data[field]]), - 'ramScoreTaken': sum([sum(heat['craft'][craft]['rammedPartsLostBy'].values()) for heat in round.values() if craft in heat['craft'] and 'rammedPartsLostBy' in heat['craft'][craft]]), - 'battleDamage': sum([data[field][craft] for heat in round.values() for player, data in heat['craft'].items() if player != craft for field in ('battleDamageBy',) if field in data and craft in data[field]]), - 'battleDamageTaken': sum([sum(heat['craft'][craft]['battleDamageBy'].values()) for heat in round.values() if craft in heat['craft'] and 'battleDamageBy' in heat['craft'][craft]]), - 'HPremaining': CalculateAvgHP(sum([heat['craft'][craft]['HPremaining'] for heat in round.values() if craft in heat['craft'] and 'HPremaining' in heat['craft'][craft] and heat['craft'][craft]['state'] == 'ALIVE']), len([1 for heat in round.values() if craft in heat['craft'] and heat['craft'][craft]['state'] == 'ALIVE'])), - 'accuracy': CalculateAccuracy(sum([heat['craft'][craft]['hits'] for heat in round.values() if craft in heat['craft'] and 'hits' in heat['craft'][craft]]), sum([heat['craft'][craft]['shots'] for heat in round.values() if craft in heat['craft'] and 'shots' in heat['craft'][craft]])), - 'rocket_accuracy': CalculateAccuracy(sum([heat['craft'][craft]['rocket_strikes'] for heat in round.values() if craft in heat['craft'] and 'rocket_strikes' in heat['craft'][craft]]), sum([heat['craft'][craft]['rockets_fired'] for heat in round.values() if craft in heat['craft'] and 'rockets_fired' in heat['craft'][craft]])), - 'waypointCount': sum(len(heat['craft'][craft]['waypoints']) for heat in round.values() if craft in heat['craft'] and 'waypoints' in heat['craft'][craft]), - 'waypointDeviation': sum(float(waypoint[1]) for heat in round.values() if craft in heat['craft'] and 'waypoints' in heat['craft'][craft] for waypoint in heat['craft'][craft]['waypoints']), - 'waypointTime': sum(float(heat['craft'][craft]['waypoints'][-1][2]) - float(heat['craft'][craft]['waypoints'][0][2]) for heat in round.values() if craft in heat['craft'] and 'waypoints' in heat['craft'][craft]), - } for round in tournamentData.values() - ] for craft in craftNames - } per_round_scores = { craft: [ sum( - w * scores[round][f][0] if isinstance(scores[round][f], tuple) else w * scores[round][f] for w, f in zip(weights, score_fields) - ) for round in range(len(scores)) + w * scores[round][f][0] if isinstance(scores[round][f], tuple) else w * scores[round][f] for w, f in zip(weights, score_fields) if not f.startswith('waypoint') + ) + + max(0, sum( + w * scores[round][f][0] if isinstance(scores[round][f], tuple) else w * scores[round][f] for w, f in zip(weights, score_fields) if f.startswith('waypoint') # Compute waypoint score separately to avoid non-negative values. + )) + for round in range(len(scores)) ] for craft, scores in per_round_summary.items() } + else: + per_round_scores = {} # Silence Pylance warnings. if not args.quiet: # Write results to console strings = [] - if not args.current_dir and 'duration' in tournamentMetadata: - strings.append(f"Tournament {tournamentMetadata.get('ID', '???')} of duration {tournamentMetadata['duration'][1]-tournamentMetadata['duration'][0]} starting at {tournamentMetadata['duration'][0]}") - headers = ['Name', 'Wins', 'Survive', 'MIA', 'Deaths (BRMRAS)', 'D.Order', 'D.Time', 'Kills (BRMR)', 'Assists', 'Hits', 'Damage', 'DmgTaken', 'RocHits', 'RocParts', 'RocDmg', 'HitByRoc', 'MisHits', 'MisParts', 'MisDmg', 'HitByMis', 'Ram', 'BD dealt', 'BD taken', 'Acc%', 'RktAcc%', 'HP%', 'Dmg/Hit', 'Hits/Sp', 'Dmg/Sp', 'WPcount', 'WPtime', 'WPdev'] if not args.scores_only else ['Name'] + if not args.no_header and not args.current_dir and 'duration' in tournamentMetadata: + strings.append(f"Tournament {tournamentMetadata.get('ID', '???')} of duration {tournamentMetadata['duration'][1]-tournamentMetadata['duration'][0]} with {tournamentMetadata['rounds']} rounds starting at {tournamentMetadata['duration'][0]}") + headers = ['Name', 'Wins', 'Survive', 'MIA', 'Deaths (BRMRAS)', 'D.Order', 'D.Time', 'Kills (BRMR)', 'Assists', 'Hits', 'Damage', 'DmgTaken', 'RocHits', 'RocParts', 'RocDmg', 'HitByRoc', 'MisHits', 'MisParts', 'MisDmg', 'HitByMis', 'Ram', 'BD dealt', 'BD taken', 'Ast.', 'Acc%', 'RktAcc%', 'HP%', 'Dmg/Hit', 'Hits/Sp', 'Dmg/Sp'] if not args.scores_only else ['Name'] + if hasWaypoints and not args.scores_only: + headers.extend(['WPcount', 'WPtime', 'WPdev', 'WPbestC', 'WPbestT', 'WPbestD']) if args.score: headers.insert(1, 'Score') summary_strings = {'header': {field: field for field in headers}} @@ -487,17 +522,24 @@ def encode_names(log_lines: List[str]) -> Tuple[Dict[str, str], List[str]]: 'Ram': f"{tmp['ramScore']}", 'BD dealt': f"{tmp['battleDamage']:.0f}", 'BD taken': f"{tmp['battleDamageTaken']:.0f}", + 'Ast.': f"{tmp['partsLostToAsteroids']}", 'Acc%': f"{tmp['accuracy']:.3g}", 'RktAcc%': f"{tmp['rocket_accuracy']:.3g}", - 'HP%': f"{tmp['HPremaining']:.2f}", + 'HP%': f"{tmp['HPremaining']:.3g}", 'Dmg/Hit': f"{tmp['damage/hit']:.1f}", 'Hits/Sp': f"{tmp['hits/spawn']:.1f}", 'Dmg/Sp': f"{tmp['damage/spawn']:.1f}", + } + }) + if hasWaypoints: + summary_strings[craft].update({ 'WPcount': f"{tmp['waypointCount']}", 'WPtime': f"{tmp['waypointTime']:.1f}", 'WPdev': f"{tmp['waypointDeviation']:.1f}", - } - }) + 'WPbestC': f"{tmp['waypointBestCount']}", + 'WPbestT': f"{tmp['waypointBestTime']:.1f}", + 'WPbestD': f"{tmp['waypointBestDeviation']:.1f}", + }) if args.score: summary_strings[craft]['Score'] = f"{tmp['score']:.3f}" columns_to_show = [header for header in headers if not all(craft[header] == "0" for craft in list(summary_strings.values())[1:])] @@ -525,7 +567,7 @@ def encode_names(log_lines: List[str]) -> Tuple[Dict[str, str], List[str]]: # Write teams results to the summary.csv file. if not args.no_files: - with open(tournamentDir / 'summary.csv', 'a') as f: + with open(tournamentDir / 'summary.csv', 'a', encoding="utf-8") as f: f.write('\n\nTeam,Wins,Draws,Deaths,Vessels') for team in sorted(teamNames, key=lambda team: teamWins[team], reverse=True): f.write('\n' + ','.join([str(v) for v in (team, teamWins[team], teamDraws[team], teamDeaths[team], summary['teams'][team].replace(", ", ","))])) diff --git a/BDArmory/Distribution/GameData/BDArmory/plot_summary.py b/BDArmory/Distribution/GameData/BDArmory/plot_summary.py index 081cf0002..a717eddd6 100755 --- a/BDArmory/Distribution/GameData/BDArmory/plot_summary.py +++ b/BDArmory/Distribution/GameData/BDArmory/plot_summary.py @@ -6,26 +6,39 @@ import sys import tempfile from pathlib import Path +from typing import Union # Third party imports import matplotlib.pyplot as plt import numpy -VERSION = "1.3" +VERSION = "1.4" parser = argparse.ArgumentParser(description="Plot the scores of a tournament as they accumulated per round", formatter_class=argparse.ArgumentDefaultsHelpFormatter) parser.add_argument("tournament", nargs="?", type=str, help="The tournament to plot (optional).") parser.add_argument('-t', '--title', type=str, help="A title.") parser.add_argument('-s', '--save', type=str, nargs='?', const='tmp', help="Save a PNG image instead of displaying the graph.") +parser.add_argument('--transparent', action='store_true', help='Save the PNG image with a transparent background.') parser.add_argument("--version", action='store_true', help="Show the script version, then exit.") +parser.add_argument("-cz", '--cut-zero', action='store_true', help="Cut the y axis off at zero to avoid large negative scores.") args = parser.parse_args() if args.version: print(f"Version: {VERSION}") sys.exit() + +def naturalSortKey(key: Union[str, Path]): + if isinstance(key, Path): + key = key.name + try: + return int(key.rsplit(' ')[1]) # If the key ends in an integer, split that off and use that as the sort key. + except: + return key # Otherwise, just use the key. + + if args.tournament is None: - tournamentFolders = sorted(list(dir for dir in ((Path(__file__).parent / "Logs").resolve().glob("Tournament*")) if dir.is_dir())) + tournamentFolders = sorted(list(dir for dir in ((Path(__file__).parent / "Logs").resolve().glob("Tournament*")) if dir.is_dir()), key=naturalSortKey) tournamentDir = tournamentFolders[-1] if len(tournamentFolders) > 0 else Path('.') else: tournamentDir = Path(args.tournament) @@ -38,6 +51,9 @@ plt.figure(figsize=(16, 10), dpi=200) plt.plot(scores.transpose(), linewidth=5) plt.legend(names, loc='upper left') +if args.cut_zero: + y0, y1 = plt.ylim() + plt.ylim(max(y0, 0), y1) if args.title is not None: plt.title(args.title) if args.save: @@ -45,7 +61,7 @@ fd, filename = tempfile.mkstemp(suffix='.png') else: filename = args.save - plt.savefig(filename, dpi='figure', bbox_inches='tight') + plt.savefig(filename, dpi='figure', bbox_inches='tight', transparent=args.transparent) print(f"Image saved to {filename}") else: plt.show(block=True) diff --git a/BDArmory/Distribution/GameData/BDArmory/plot_vessel_traces.py b/BDArmory/Distribution/GameData/BDArmory/plot_vessel_traces.py index 7a87d203f..5c7a6e9b9 100644 --- a/BDArmory/Distribution/GameData/BDArmory/plot_vessel_traces.py +++ b/BDArmory/Distribution/GameData/BDArmory/plot_vessel_traces.py @@ -6,7 +6,6 @@ # Third party imports import matplotlib.pyplot as plt -from mpl_toolkits.mplot3d import Axes3D VERSION = "1.0" diff --git a/BDArmory/Evolution/BDAEvolution.cs b/BDArmory/Evolution/BDAEvolution.cs index 5f896f7b3..10dec1327 100644 --- a/BDArmory/Evolution/BDAEvolution.cs +++ b/BDArmory/Evolution/BDAEvolution.cs @@ -3,11 +3,12 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using BDArmory.Competition; -using BDArmory.Competition.VesselSpawning; -using BDArmory.Core; using UnityEngine; +using BDArmory.Competition; +using BDArmory.Settings; +using BDArmory.VesselSpawning; + namespace BDArmory.Evolution { public enum EvolutionStatus @@ -36,7 +37,7 @@ public class EvolutionWorkingState { public string savegame; public string evolutionId; - public SpawnConfig spawnConfig; + public CircularSpawnConfig spawnConfig; // public Dictionary> aggregateScores; } @@ -111,7 +112,7 @@ public class BDAModuleEvolution : MonoBehaviour private VariantEngine engine = null; // Spawn settings - private static SpawnConfig spawnConfig; + private static CircularSpawnConfig spawnConfig; // config node for evolution details private ConfigNode config = null; @@ -184,20 +185,22 @@ public void StartEvolution() nextVariantId = 1; groupId = 1; evolutionId = DateTimeOffset.Now.ToUnixTimeSeconds().ToString(); - spawnConfig = new SpawnConfig( - BDArmorySettings.VESSEL_SPAWN_WORLDINDEX, - BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.x, - BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.y, - BDArmorySettings.VESSEL_SPAWN_ALTITUDE, + spawnConfig = new CircularSpawnConfig( + new SpawnConfig( + BDArmorySettings.VESSEL_SPAWN_WORLDINDEX, + BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.x, + BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.y, + BDArmorySettings.VESSEL_SPAWN_ALTITUDE, + true, + true, + 0, + null, + null, + workingDirectory + ), BDArmorySettings.VESSEL_SPAWN_DISTANCE_TOGGLE ? BDArmorySettings.VESSEL_SPAWN_DISTANCE : BDArmorySettings.VESSEL_SPAWN_DISTANCE_FACTOR, - BDArmorySettings.VESSEL_SPAWN_DISTANCE_TOGGLE, - BDArmorySettings.VESSEL_SPAWN_EASE_IN_SPEED, - true, - true, - 0, - null, - null, - workingDirectory); + BDArmorySettings.VESSEL_SPAWN_DISTANCE_TOGGLE + ); evolutionState = new EvolutionState(evolutionId, status, new List()); // create new config @@ -326,7 +329,7 @@ public static EvolutionWorkingState LoadState() { var strings = File.ReadAllLines(stateFile); state = JsonUtility.FromJson(strings[0]); - state.spawnConfig = JsonUtility.FromJson(strings[1]); + state.spawnConfig = JsonUtility.FromJson(strings[1]); } catch (Exception e) { @@ -524,7 +527,7 @@ private IEnumerator ExecuteTournament() } yield return wait; - BDACompetitionMode.Instance.StartCompetitionMode(BDArmorySettings.COMPETITION_DISTANCE); + BDACompetitionMode.Instance.StartCompetitionMode(BDArmorySettings.COMPETITION_DISTANCE, BDArmorySettings.COMPETITION_START_DESPITE_FAILURES); yield return new WaitForSeconds(5); // wait 5sec for stability while (BDACompetitionMode.Instance.competitionStarting || BDACompetitionMode.Instance.competitionIsActive) diff --git a/BDArmory/Evolution/ControlSurfaceAxisNudgeMutation.cs b/BDArmory/Evolution/ControlSurfaceAxisNudgeMutation.cs index 080803154..ca3e1afe2 100644 --- a/BDArmory/Evolution/ControlSurfaceAxisNudgeMutation.cs +++ b/BDArmory/Evolution/ControlSurfaceAxisNudgeMutation.cs @@ -29,7 +29,7 @@ public ControlSurfaceAxisNudgeMutation(string paramName, float modifier, int axi public void Apply(ConfigNode craft, VariantEngine engine) { - Debug.Log("Evolution ControlSurfaceNudgeMutation applying"); + Debug.Log("[BDArmory.ControlSurfaceAxisNudgeMutation]: Evolution ControlSurfaceNudgeMutation applying"); List matchingNodes = engine.FindModuleNodes(craft, moduleName); foreach (var node in matchingNodes) { diff --git a/BDArmory/Evolution/ControlSurfaceNudgeMutation.cs b/BDArmory/Evolution/ControlSurfaceNudgeMutation.cs index b5a53638f..b8e1c0660 100644 --- a/BDArmory/Evolution/ControlSurfaceNudgeMutation.cs +++ b/BDArmory/Evolution/ControlSurfaceNudgeMutation.cs @@ -27,7 +27,7 @@ public ControlSurfaceNudgeMutation(string[] partNames, string paramName, float m public void Apply(ConfigNode craft, VariantEngine engine) { - Debug.Log("Evolution ControlSurfaceNudgeMutation applying"); + Debug.Log("[BDArmory.ControlSurfaceNudgeMutation]: Evolution ControlSurfaceNudgeMutation applying"); Dictionary matchingNodeMap = new Dictionary(); foreach (var partName in partNames) { diff --git a/BDArmory/Evolution/EngineGimbalAxisNudgeMutation.cs b/BDArmory/Evolution/EngineGimbalAxisNudgeMutation.cs index 2db37eade..a0513bab2 100644 --- a/BDArmory/Evolution/EngineGimbalAxisNudgeMutation.cs +++ b/BDArmory/Evolution/EngineGimbalAxisNudgeMutation.cs @@ -28,7 +28,7 @@ public EngineGimbalAxisNudgeMutation(string paramName, float modifier, int axisM public void Apply(ConfigNode craft, VariantEngine engine) { - Debug.Log("Evolution EngineGimbalNudgeMutation applying"); + Debug.Log("[BDArmory.EngineGimbalAxisNudgeMutation]: Evolution EngineGimbalNudgeMutation applying"); List matchingModules = engine.FindModuleNodes(craft, moduleName); foreach (var node in matchingModules) { @@ -76,7 +76,7 @@ private void MutateIfNeeded(ConfigNode node, ConfigNode craft, VariantEngine eng ConfigNode partNode = engine.FindParentPart(craft, node); if( partNode == null ) { - Debug.Log("Evolution EngineGimbalNudgeMutation failed to find parent part for module"); + Debug.Log("[BDArmory.EngineGimbalAxisNudgeMutation]: Evolution EngineGimbalNudgeMutation failed to find parent part for module"); return; } string partName = partNode.GetValue("part"); diff --git a/BDArmory/Evolution/EngineGimbalNudgeMutation.cs b/BDArmory/Evolution/EngineGimbalNudgeMutation.cs index 8b4a4d570..dacecda57 100644 --- a/BDArmory/Evolution/EngineGimbalNudgeMutation.cs +++ b/BDArmory/Evolution/EngineGimbalNudgeMutation.cs @@ -26,7 +26,7 @@ public EngineGimbalNudgeMutation(string[] partNames, string paramName, float mod public void Apply(ConfigNode craft, VariantEngine engine) { - Debug.Log("Evolution EngineGimbalNudgeMutation applying"); + Debug.Log("[BDArmory.EngineGimbalNudgeMutation]: Evolution EngineGimbalNudgeMutation applying"); Dictionary matchingNodeMap = new Dictionary(); foreach (var partName in partNames) { diff --git a/BDArmory/Evolution/EvolutionWindow.cs b/BDArmory/Evolution/EvolutionWindow.cs index 8d7830497..ac6ec0880 100644 --- a/BDArmory/Evolution/EvolutionWindow.cs +++ b/BDArmory/Evolution/EvolutionWindow.cs @@ -1,9 +1,10 @@ using System.Collections; using UnityEngine; -using BDArmory.Core; using KSP.Localization; -using BDArmory.Misc; + +using BDArmory.Settings; using BDArmory.UI; +using BDArmory.Utils; namespace BDArmory.Evolution { @@ -13,7 +14,7 @@ public class EvolutionWindow : MonoBehaviour public static EvolutionWindow Instance; private BDAModuleEvolution evolution; - private int _guiCheckIndex; + private static int _guiCheckIndex = -1; private static readonly float _buttonSize = 20; private static readonly float _margin = 5; private static readonly float _lineHeight = _buttonSize; @@ -42,7 +43,6 @@ Rect SRightSliderRect(float line) private void Awake() { - // Debug.Log("EvolutionWindow awake"); if (Instance) Destroy(this); Instance = this; @@ -50,7 +50,6 @@ private void Awake() private void Start() { - // Debug.Log("EvolutionWindow start"); leftLabel = new GUIStyle(); leftLabel.alignment = TextAnchor.UpperLeft; leftLabel.normal.textColor = Color.white; @@ -81,14 +80,16 @@ private void OnGUI() _windowWidth, _windowHeight ); + BDArmorySetup.SetGUIOpacity(); BDArmorySetup.WindowRectEvolution = GUI.Window( 8008135, BDArmorySetup.WindowRectEvolution, WindowEvolution, - Localizer.Format("#LOC_BDArmory_Evolution_Title"),//"BDA Evolution" + StringUtils.Localize("#LOC_BDArmory_Evolution_Title"),//"BDA Evolution" BDArmorySetup.BDGuiSkin.window ); - Utils.UpdateGUIRect(BDArmorySetup.WindowRectEvolution, _guiCheckIndex); + BDArmorySetup.SetGUIOpacity(false); + GUIUtils.UpdateGUIRect(BDArmorySetup.WindowRectEvolution, _guiCheckIndex); } private void SetNewHeight(float windowHeight) @@ -96,6 +97,12 @@ private void SetNewHeight(float windowHeight) BDArmorySetup.WindowRectEvolution.height = windowHeight; } + public void SetVisible(bool visible) + { + BDArmorySetup.Instance.showEvolutionGUI = visible; + GUIUtils.SetGUIRectVisible(_guiCheckIndex, visible); + } + private IEnumerator WaitForSetup() { while (BDArmorySetup.Instance == null || BDAModuleEvolution.Instance == null) @@ -104,10 +111,9 @@ private IEnumerator WaitForSetup() } evolution = BDAModuleEvolution.Instance; - // Debug.Log("EvolutionWindow ready"); BDArmorySetup.Instance.hasEvolution = true; ready = true; - _guiCheckIndex = Utils.RegisterGUIRect(new Rect()); + if (_guiCheckIndex < 0) _guiCheckIndex = GUIUtils.RegisterGUIRect(new Rect()); } private void WindowEvolution(int id) @@ -116,7 +122,7 @@ private void WindowEvolution(int id) float offset = _titleHeight + _margin; GUI.DragWindow(new Rect(0, 0, BDArmorySettings.EVOLUTION_WINDOW_WIDTH - _titleHeight / 2 - 2, _titleHeight)); - if (GUI.Button(SLineRect(++line), (BDArmorySettings.SHOW_EVOLUTION_OPTIONS ? "Hide " : "Show ") + Localizer.Format("#LOC_BDArmory_Evolution_Options"), BDArmorySettings.SHOW_EVOLUTION_OPTIONS ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))//Show/hide evolution options + if (GUI.Button(SLineRect(++line), (BDArmorySettings.SHOW_EVOLUTION_OPTIONS ? StringUtils.Localize("#LOC_BDArmory_Generic_Hide") : StringUtils.Localize("#LOC_BDArmory_Generic_Show")) + " " + StringUtils.Localize("#LOC_BDArmory_Evolution_Options"), BDArmorySettings.SHOW_EVOLUTION_OPTIONS ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))//Show/hide evolution options { BDArmorySettings.SHOW_EVOLUTION_OPTIONS = !BDArmorySettings.SHOW_EVOLUTION_OPTIONS; } @@ -124,19 +130,19 @@ private void WindowEvolution(int id) { int mutationsPerHeat = BDArmorySettings.EVOLUTION_MUTATIONS_PER_HEAT; var mphDisplayValue = BDArmorySettings.EVOLUTION_MUTATIONS_PER_HEAT.ToString("0"); - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Evolution_MutationsPerHeat")}: ({mphDisplayValue})", leftLabel);//Mutations Per Heat + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Evolution_MutationsPerHeat")}: ({mphDisplayValue})", leftLabel);//Mutations Per Heat mutationsPerHeat = (int)GUI.HorizontalSlider(SRightSliderRect(line), mutationsPerHeat, 1, 10); BDArmorySettings.EVOLUTION_MUTATIONS_PER_HEAT = mutationsPerHeat; int adversariesPerHeat = BDArmorySettings.EVOLUTION_ANTAGONISTS_PER_HEAT; var aphDisplayValue = BDArmorySettings.EVOLUTION_ANTAGONISTS_PER_HEAT.ToString("0"); - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Evolution_AdversariesPerHeat")}: ({aphDisplayValue})", leftLabel);//Adversaries Per Heat + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Evolution_AdversariesPerHeat")}: ({aphDisplayValue})", leftLabel);//Adversaries Per Heat adversariesPerHeat = (int)GUI.HorizontalSlider(SRightSliderRect(line), adversariesPerHeat, 0, 10); BDArmorySettings.EVOLUTION_ANTAGONISTS_PER_HEAT = adversariesPerHeat; int heatsPerGroup = BDArmorySettings.EVOLUTION_HEATS_PER_GROUP; var hpgDisplayValue = BDArmorySettings.EVOLUTION_HEATS_PER_GROUP.ToString("0"); - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Evolution_HeatsPerGroup")}: ({hpgDisplayValue})", leftLabel);//Heats Per Group + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Evolution_HeatsPerGroup")}: ({hpgDisplayValue})", leftLabel);//Heats Per Group heatsPerGroup = (int)GUI.HorizontalSlider(SRightSliderRect(line), heatsPerGroup, 1, 50); BDArmorySettings.EVOLUTION_HEATS_PER_GROUP = heatsPerGroup; @@ -145,10 +151,10 @@ private void WindowEvolution(int id) float fifth = _windowWidth / 5.0f; offset += 0.25f * _lineHeight; - GUI.Label(new Rect(_margin, offset, 2 * fifth, _lineHeight), $"{Localizer.Format("#LOC_BDArmory_Evolution_ID")}: "); + GUI.Label(new Rect(_margin, offset, 2 * fifth, _lineHeight), $"{StringUtils.Localize("#LOC_BDArmory_Evolution_ID")}: "); GUI.Label(new Rect(_margin + 2 * fifth, offset, 3 * fifth, _lineHeight), evolution.EvolutionId); offset += _lineHeight; - GUI.Label(new Rect(_margin, offset, 2 * fifth, _lineHeight), $"{Localizer.Format("#LOC_BDArmory_Evolution_Status")}: "); + GUI.Label(new Rect(_margin, offset, 2 * fifth, _lineHeight), $"{StringUtils.Localize("#LOC_BDArmory_Evolution_Status")}: "); string statusLine; switch (evolution.Status()) { @@ -158,9 +164,9 @@ private void WindowEvolution(int id) } GUI.Label(new Rect(_margin + 2 * fifth, offset, 3 * fifth, _lineHeight), statusLine); offset += _lineHeight; - GUI.Label(new Rect(_margin, offset, fifth, _lineHeight), $"{Localizer.Format("#LOC_BDArmory_Evolution_Group")}: "); + GUI.Label(new Rect(_margin, offset, fifth, _lineHeight), $"{StringUtils.Localize("#LOC_BDArmory_Evolution_Group")}: "); GUI.Label(new Rect(_margin + fifth, offset, fifth, _lineHeight), evolution.GroupId.ToString()); - GUI.Label(new Rect(_margin + 2 * fifth, offset, fifth, _lineHeight), $"{Localizer.Format("#LOC_BDArmory_Evolution_Heat")}: "); + GUI.Label(new Rect(_margin + 2 * fifth, offset, fifth, _lineHeight), $"{StringUtils.Localize("#LOC_BDArmory_Evolution_Heat")}: "); GUI.Label(new Rect(_margin + 3 * fifth, offset, fifth, _lineHeight), evolution.Heat.ToString()); offset += _lineHeight; string buttonText; @@ -177,7 +183,6 @@ private void WindowEvolution(int id) } if (GUI.Button(new Rect(_margin, offset, nextButton ? 2 * _windowWidth / 3 - _margin : _windowWidth - 2 * _margin, _lineHeight), buttonText, BDArmorySetup.BDGuiSkin.button)) { - // Debug.Log("EvolutionWindow buttonClicked"); switch (status) { case EvolutionStatus.Idle: @@ -192,7 +197,7 @@ private void WindowEvolution(int id) _windowHeight = offset; - BDGUIUtils.RepositionWindow(ref BDArmorySetup.WindowRectEvolution); // Prevent it from going off the screen edges. + GUIUtils.RepositionWindow(ref BDArmorySetup.WindowRectEvolution); // Prevent it from going off the screen edges. } } } diff --git a/BDArmory/Evolution/PilotAIMutation.cs b/BDArmory/Evolution/PilotAIMutation.cs index 2b45db2e0..942aa6b75 100644 --- a/BDArmory/Evolution/PilotAIMutation.cs +++ b/BDArmory/Evolution/PilotAIMutation.cs @@ -34,7 +34,7 @@ public void Apply(ConfigNode craft, VariantEngine engine) } else { - Debug.Log("Evolution PilotAIMutation wrong number of pilot modules"); + Debug.Log("[BDArmory.PilotAIMutation]: Evolution PilotAIMutation wrong number of pilot modules"); } } diff --git a/BDArmory/Evolution/PilotAINudgeMutation.cs b/BDArmory/Evolution/PilotAINudgeMutation.cs index f9ae54f0f..f87c89c06 100644 --- a/BDArmory/Evolution/PilotAINudgeMutation.cs +++ b/BDArmory/Evolution/PilotAINudgeMutation.cs @@ -22,11 +22,11 @@ public PilotAINudgeMutation(string paramName, float modifier, string key, int di public void Apply(ConfigNode craft, VariantEngine engine) { - Debug.Log("Evolution PilotAINudgeMutation applying"); + Debug.Log("[BDArmory.PilotAINudgeMutation]: Evolution PilotAINudgeMutation applying"); List matchingNodes = engine.FindModuleNodes(craft, "BDModulePilotAI"); if (matchingNodes.Count == 1) { - Debug.Log("Evolution PilotAINudgeMutation found module"); + Debug.Log("[BDArmory.PilotAINudgeMutation]: Evolution PilotAINudgeMutation found module"); var node = matchingNodes[0]; float existingValue; float.TryParse(node.GetValue(paramName), out existingValue); @@ -36,7 +36,7 @@ public void Apply(ConfigNode craft, VariantEngine engine) ConfigNode partNode = engine.FindParentPart(craft, node); if( partNode == null ) { - Debug.Log("Evolution PilotAINudgeMutation failed to find parent part for module"); + Debug.Log("[BDArmory.PilotAINudgeMutation]: Evolution PilotAINudgeMutation failed to find parent part for module"); return; } string partName = partNode.GetValue("part"); @@ -51,7 +51,7 @@ public void Apply(ConfigNode craft, VariantEngine engine) } else { - Debug.Log("Evolution PilotAINudgeMutation wrong number of pilot modules"); + Debug.Log("[BDArmory.PilotAINudgeMutation]: Evolution PilotAINudgeMutation wrong number of pilot modules"); } } diff --git a/BDArmory/Evolution/VariantEngine.cs b/BDArmory/Evolution/VariantEngine.cs index b3b42145a..51b4151bc 100644 --- a/BDArmory/Evolution/VariantEngine.cs +++ b/BDArmory/Evolution/VariantEngine.cs @@ -58,7 +58,6 @@ public class VariantEngine "vesselCollisionAvoidanceLookAheadPeriod", "vesselCollisionAvoidanceStrength", "vesselStandoffDistance", - // "extendMult", "extendDistanceAirToAir", "extendDistanceAirToGroundGuns", "extendDistanceAirToGround", diff --git a/BDArmory/Evolution/WeaponManagerMutation.cs b/BDArmory/Evolution/WeaponManagerMutation.cs index 6e7f0e07b..1bfc5e70c 100644 --- a/BDArmory/Evolution/WeaponManagerMutation.cs +++ b/BDArmory/Evolution/WeaponManagerMutation.cs @@ -38,7 +38,7 @@ public void Apply(ConfigNode craft, VariantEngine engine) } else { - Debug.Log("Evolution WeaponManagerMutation wrong number of weapon managers"); + Debug.Log("[BDArmory.WeaponManagerMutation]: Evolution WeaponManagerMutation wrong number of weapon managers"); } } diff --git a/BDArmory/Evolution/WeaponManagerNudgeMutation.cs b/BDArmory/Evolution/WeaponManagerNudgeMutation.cs index c3864449f..e9529a81e 100644 --- a/BDArmory/Evolution/WeaponManagerNudgeMutation.cs +++ b/BDArmory/Evolution/WeaponManagerNudgeMutation.cs @@ -23,36 +23,36 @@ public WeaponManagerNudgeMutation(string paramName, float modifier, string key, public void Apply(ConfigNode craft, VariantEngine engine) { - Debug.Log("Evolution WeaponManagerNudgeMutation applying"); + Debug.Log("[BDArmory.WeaponManagerNudgeMutation]: Evolution WeaponManagerNudgeMutation applying"); List matchingNodes = engine.FindModuleNodes(craft, "MissileFire"); if (matchingNodes.Count == 1) { - Debug.Log("Evolution WeaponManagerNudgeMutation found module"); + Debug.Log("[BDArmory.WeaponManagerNudgeMutation]: Evolution WeaponManagerNudgeMutation found module"); var node = matchingNodes[0]; float existingValue; float.TryParse(node.GetValue(paramName), out existingValue); - Debug.Log(string.Format("Evolution WeaponManagerNudgeMutation found existing value {0} = {1}", paramName, existingValue)); + Debug.Log(string.Format("[BDArmory.WeaponManagerNudgeMutation]: Evolution WeaponManagerNudgeMutation found existing value {0} = {1}", paramName, existingValue)); if (engine.NudgeNode(node, paramName, modifier)) { ConfigNode partNode = engine.FindParentPart(craft, node); if( partNode == null ) { - Debug.Log("Evolution WeaponManagerNudgeMutation failed to find parent part for module"); + Debug.Log("[BDArmory.WeaponManagerNudgeMutation]: Evolution WeaponManagerNudgeMutation failed to find parent part for module"); return; } string partName = partNode.GetValue("part"); var value = existingValue * (1 + modifier); - Debug.Log(string.Format("Evolution WeaponManagerNudgeMutation mutated part {0}, module {1}, param {2}, existing: {3}, value: {4}", partName, moduleName, paramName, existingValue, value)); + Debug.Log(string.Format("[BDArmory.WeaponManagerNudgeMutation]: Evolution WeaponManagerNudgeMutation mutated part {0}, module {1}, param {2}, existing: {3}, value: {4}", partName, moduleName, paramName, existingValue, value)); mutatedParts.Add(new MutatedPart(partName, moduleName, paramName, existingValue, value)); } else { - Debug.Log(string.Format("Evolution WeaponManagerNudgeMutation unable to mutate {0}", paramName)); + Debug.Log(string.Format("[BDArmory.WeaponManagerNudgeMutation]: Evolution WeaponManagerNudgeMutation unable to mutate {0}", paramName)); } } else { - Debug.Log("Evolution WeaponManagerNudgeMutation wrong number of weapon managers"); + Debug.Log("[BDArmory.WeaponManagerNudgeMutation]: Evolution WeaponManagerNudgeMutation wrong number of weapon managers"); } } diff --git a/BDArmory/Competition/BodyExtensions.cs b/BDArmory/Extensions/BodyExtensions.cs similarity index 97% rename from BDArmory/Competition/BodyExtensions.cs rename to BDArmory/Extensions/BodyExtensions.cs index ecaa269fa..4a711c35e 100644 --- a/BDArmory/Competition/BodyExtensions.cs +++ b/BDArmory/Extensions/BodyExtensions.cs @@ -1,5 +1,6 @@ -using System; -namespace UnityEngine +using UnityEngine; + +namespace BDArmory.Extensions { public static class BodyExtensions { diff --git a/BDArmory/Extensions/ClawExtensions.cs b/BDArmory/Extensions/ClawExtensions.cs new file mode 100644 index 000000000..a56ecb31d --- /dev/null +++ b/BDArmory/Extensions/ClawExtensions.cs @@ -0,0 +1,75 @@ +using UnityEngine; +using System.Collections; + +using BDArmory.Utils; + +namespace BDArmory.Extensions +{ + public class ClawExtension : PartModule + { + [KSPAction("Toggle Free Pivot")] + public void AGToggleFreePivot(KSPActionParam param) + { + SetFreePivot(Toggle.Toggle); + } + + [KSPAction("Enable Free Pivot")] + public void AGEnableFreePivot(KSPActionParam param) + { + SetFreePivot(Toggle.On); + } + + [KSPAction("Disble Free Pivot")] + public void AGDisableFreePivot(KSPActionParam param) + { + SetFreePivot(Toggle.Off); + } + + void SetFreePivot(Toggle state) + { + var claw = part.GetComponent(); + if (claw == null) return; + if (claw.state != "Grappled") return; + switch (state) + { + case Toggle.Toggle: + if (claw.IsLoose()) + claw.LockPivot(); + else + claw.SetLoose(); + break; + case Toggle.On: + claw.SetLoose(); + break; + case Toggle.Off: + claw.LockPivot(); + break; + } + } + + [KSPAction("Enable Free Pivot When Grappled (10s)")] + public void AGEnableFreePivotWhenGrappled(KSPActionParam param) + { + StartCoroutine(EnableFreePivotWhenGrappled()); + } + + IEnumerator EnableFreePivotWhenGrappled() + { + var claw = part.GetComponent(); + var wait = new WaitForFixedUpdate(); + var tic = Time.time; + while (claw != null && (claw.state != "Grappled" || !claw.IsLoose()) && Time.time - tic < 10) // Abort after 10s + { + if (claw.state == "Grappled" && !claw.IsLoose()) claw.SetLoose(); + yield return wait; + } + } + + [KSPAction("Unlimited Pivot Range")] + public void AGUnlimitedPivotRange(KSPActionParam param) + { + var claw = part.GetComponent(); + claw.pivotRange = 180f; + } + } +} \ No newline at end of file diff --git a/BDArmory.Core/Extension/CrewExtensions.cs b/BDArmory/Extensions/CrewExtensions.cs similarity index 95% rename from BDArmory.Core/Extension/CrewExtensions.cs rename to BDArmory/Extensions/CrewExtensions.cs index c7ecc4237..28ea92baf 100644 --- a/BDArmory.Core/Extension/CrewExtensions.cs +++ b/BDArmory/Extensions/CrewExtensions.cs @@ -1,4 +1,4 @@ -namespace BDArmory.Core.Extension +namespace BDArmory.Extensions { public static class CrewExtensions { @@ -8,6 +8,7 @@ public static class CrewExtensions /// The crew member public static void ResetInventory(this ProtoCrewMember crew, bool withJetpack = false) { + if (crew == null) return; if ((Versioning.version_major == 1 && Versioning.version_minor > 10) || Versioning.version_major > 1) // Introduced in 1.11 { crew.ResetInventory_1_11(withJetpack); diff --git a/BDArmory.Core/Extension/PartExtensions.cs b/BDArmory/Extensions/PartExtensions.cs similarity index 88% rename from BDArmory.Core/Extension/PartExtensions.cs rename to BDArmory/Extensions/PartExtensions.cs index 69c856466..ccfd37db0 100644 --- a/BDArmory.Core/Extension/PartExtensions.cs +++ b/BDArmory/Extensions/PartExtensions.cs @@ -1,11 +1,11 @@ -using System; -using System.Collections.Generic; -using BDArmory.Core.Services; -using BDArmory.Core.Utils; -using UniLinq; +using System.Collections.Generic; using UnityEngine; -namespace BDArmory.Core.Extension +using BDArmory.Damage; +using BDArmory.Initialization; +using BDArmory.Settings; + +namespace BDArmory.Extensions { public enum ExplosionSourceType { Other, Missile, Bullet, Rocket, BattleDamage }; public static class PartExtensions @@ -13,6 +13,7 @@ public static class PartExtensions public static void AddDamage(this Part p, float damage) { if (BDArmorySettings.PAINTBALL_MODE) return; // Don't add damage when paintball mode is enabled + damage *= (BDArmorySettings.DMG_MULTIPLIER / 100); if (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.ZOMBIE_MODE) { if (p.vessel.rootPart != null) @@ -34,7 +35,7 @@ public static void AddDamage(this Part p, float damage) else { Dependencies.Get().AddDamageToPart_svc(p, damage); - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_ARMOR || BDArmorySettings.DEBUG_DAMAGE) Debug.Log($"[BDArmory.PartExtensions]: Standard Hitpoints Applied to {p.name}" + (p.vessel != null ? $" on {p.vessel.vesselName}" : "") + $" : {damage}"); } } @@ -51,7 +52,7 @@ public static void AddInstagibDamage(this Part p) { p.vessel.rootPart.Destroy(); } - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_ARMOR || BDArmorySettings.DEBUG_DAMAGE) Debug.Log("[BDArmory.PartExtensions]: Instagib!"); } } @@ -110,7 +111,6 @@ public static float AddExplosiveDamage(this Part p, damage_ = damageReduction; } - ////////////////////////////////////////////////////////// // Apply Hitpoints ////////////////////////////////////////////////////////// @@ -186,7 +186,6 @@ public static float AddBallisticDamage(this Part p, break; } - var damage_before = damage_; ////////////////////////////////////////////////////////// // Armor Reduction factors @@ -235,10 +234,9 @@ public static void ApplyHitPoints(Part p, float damage_, float caliber, float ma // Apply HitPoints Ballistic ////////////////////////////////////////////////////////// Dependencies.Get().AddDamageToPart_svc(p, damage_); - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_ARMOR || BDArmorySettings.DEBUG_DAMAGE) { Debug.Log("[BDArmory.PartExtensions]: mass: " + mass + " caliber: " + caliber + " multiplier: " + multiplier + " velocity: " + impactVelocity + " penetrationfactor: " + penetrationfactor); - Debug.Log("[BDArmory.PartExtensions]: Ballistic Hitpoints Applied to " + p.name + ": " + damage_); } } public static void AddHealth(this Part p, float healing, bool overcharge = false) @@ -250,7 +248,7 @@ public static void AddHealth(this Part p, float healing, bool overcharge = false else { Dependencies.Get().AddHealthToPart_svc(p, healing, overcharge); - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_ARMOR || BDArmorySettings.DEBUG_DAMAGE) Debug.Log($"[BDArmory.PartExtensions]: Standard Hitpoints Restored to {p.name}" + (p.vessel != null ? $" on {p.vessel.vesselName}" : "") + $" : {healing}"); } } @@ -264,7 +262,7 @@ public static void ApplyHitPoints(Part p, float damage) ////////////////////////////////////////////////////////// Dependencies.Get().AddDamageToPart_svc(p, damage); - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_ARMOR || BDArmorySettings.DEBUG_DAMAGE) Debug.Log("[BDArmory.PartExtensions]: Explosive Hitpoints Applied to " + p.name + ": " + damage); } @@ -278,7 +276,7 @@ public static void ApplyHitPoints(KerbalEVA kerbal, float damage) ////////////////////////////////////////////////////////// Dependencies.Get().AddDamageToKerbal_svc(kerbal, damage); - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_ARMOR || BDArmorySettings.DEBUG_DAMAGE) Debug.Log("[BDArmory.PartExtensions]: Hitpoints Applied to " + kerbal.name + ": " + damage); } @@ -329,7 +327,7 @@ public static void ReduceArmor(this Part p, double massToReduce) //massToReduce = Math.Max(0.10, Math.Round(massToReduce, 2)); Dependencies.Get().ReduceArmor_svc(p, (float)massToReduce); - if (BDArmorySettings.DRAW_ARMOR_LABELS) + if (BDArmorySettings.DEBUG_ARMOR) { //Debug.Log("[BDArmory.PartExtensions]: Armor volume Removed : " + massToReduce); } @@ -341,12 +339,12 @@ public static float GetArmorThickness(this Part p) float armorthickness = Dependencies.Get().GetPartArmor_svc(p); if (float.IsNaN(armorthickness)) { - if (BDArmorySettings.DRAW_ARMOR_LABELS) Debug.Log("[PartExtensions] GetArmorThickness; thickness is NaN"); + if (BDArmorySettings.DEBUG_ARMOR) Debug.Log("[BDArmory.PartExtensions]: GetArmorThickness; thickness is NaN"); return 0f; } else { - //if (BDArmorySettings.DRAW_ARMOR_LABELS) Debug.Log("[PartExtensions] GetArmorThickness; thickness is: " + armorthickness); + //if (BDArmorySettings.DEBUG_ARMOR) Debug.Log("[BDArmory.PartExtensions]: GetArmorThickness; thickness is: " + armorthickness); return armorthickness; } //return Dependencies.Get().GetPartArmor_svc(p); @@ -407,8 +405,22 @@ public static void RefreshAssociatedWindows(this Part part) public static bool IsMissile(this Part part) { - return part.Modules.Contains("MissileBase") || part.Modules.Contains("MissileLauncher") || - part.Modules.Contains("BDModularGuidance"); + if (part.Modules.Contains("BDModularGuidance")) return true; + if (part.Modules.Contains("MissileBase") || part.Modules.Contains("MissileLauncher")) + { + if (!part.Modules.Contains("MultiMissileLauncher")) return true; + IEnumerator partModules = part.Modules.GetEnumerator(); + while (partModules.MoveNext()) + { + if (partModules.Current.moduleName == "MultiMissileLauncher") + { + return (((Weapons.Missiles.MultiMissileLauncher)partModules.Current).isClusterMissile); + } + } + //return ((part.Modules.Contains("MissileBase") || part.Modules.Contains("MissileLauncher") || + // part.Modules.Contains("BDModularGuidance")) + } + return false; } public static bool IsWeapon(this Part part) { @@ -493,7 +505,12 @@ public static bool IsAero(this Part part) } else return false; } - + public static bool IsMotor(this Part part) + { + if (part.GetComponent() != null || part.GetComponent() != null) + return true; + else return false; + } public static string GetExplodeMode(this Part part) { return Dependencies.Get().GetExplodeMode_svc(part); @@ -550,19 +567,19 @@ public static float DamageReduction(float armor, float density, float strength, { case ExplosionSourceType.Missile: //damage *= Mathf.Clamp(-0.0005f * armor + 1.025f, 0f, 0.5f); // Cap damage reduction at 50% (armor = 1050) - if (BDArmorySettings.DRAW_ARMOR_LABELS) + if (BDArmorySettings.DEBUG_ARMOR) { Debug.Log("[BDArmory.PartExtensions]: Damage Before Reduction : " + damage + "; Damage Reduction (%) : " + 1 + (((strength * (density / 1000)) * armor) / 1000000) + "; Damage After Armor : " + (damage / (1 + (((strength * (density / 1000)) * armor) / 1000000)))); } - damage /= 1 + (((strength * (density / 1000)) * armor) / 1000000); //500mm of DU yields about 95% reduction, 500mm steel = 80% reduction, Aramid = 73% reduction + damage /= 1 + (((strength * (density / 1000)) * armor) / 1000000); //500mm of DU yields about 95% reduction, 500mm steel = 80% reduction, Aramid = 73% reduction, if explosion makes it past armor break; case ExplosionSourceType.BattleDamage: //identical to missile for now, since fuel/ammo explosions can be mitigated by armor mass - if (BDArmorySettings.DRAW_ARMOR_LABELS) + if (BDArmorySettings.DEBUG_ARMOR) { Debug.Log("[BDArmory.PartExtensions]: Damage Before Reduction : " + damage + "; Damage Reduction (%) : " + 1 + (((strength * (density / 1000)) * armor) / 1000000) @@ -591,7 +608,7 @@ public static float DamageReduction(float armor, float density, float strength, _damageReduction = (113 * armor) / (154 + armor); //should look at this later, review? - if (BDArmorySettings.DRAW_ARMOR_LABELS) + if (BDArmorySettings.DEBUG_ARMOR) { Debug.Log("[BDArmory.PartExtensions]: Damage Before Reduction : " + damage + "; Damage Reduction (%) : " + 100 * (1 - Mathf.Clamp01((113f - _damageReduction) / 100f)) @@ -654,5 +671,33 @@ private static bool IsKerbalEVA_1_10(this Part part) { return part.FindModuleImplementing() != null; } + + /// + /// KSP version dependent query of whether the part is a kerbal seat. + /// + /// Part to check. + /// true if the part is a kerbal seat. + public static bool IsKerbalSeat(this Part part) + { + if (part == null) return false; + if ((Versioning.version_major == 1 && Versioning.version_minor > 10) || Versioning.version_major > 1) // Introduced in 1.11 + { + return part.IsKerbalSeat_1_11(); + } + else + { + return part.IsKerbalSeat_1_10(); + } + } + + private static bool IsKerbalSeat_1_11(this Part part) // KSP has issues on older versions if this call is in the parent function. + { + return part.isKerbalSeat(); + } + + private static bool IsKerbalSeat_1_10(this Part part) + { + return part.FindModuleImplementing() != null; + } } } \ No newline at end of file diff --git a/BDArmory/Extensions/VectorExtensions.cs b/BDArmory/Extensions/VectorExtensions.cs new file mode 100644 index 000000000..5f4e88369 --- /dev/null +++ b/BDArmory/Extensions/VectorExtensions.cs @@ -0,0 +1,77 @@ +using UnityEngine; +using System.Runtime.CompilerServices; + +namespace BDArmory.Extensions +{ + public static class VectorExtensions + { + /// + /// Project a vector onto a plane defined by the plane normal (pre-normalized). + /// + /// This implementation assumes that the plane normal is already normalized, + /// skipping such checks and normalization that Vector3.ProjectOnPlane does, + /// which gives a speed-up by a factor of approximately 1.7. + /// + /// The vector to project. + /// The plane normal (pre-normalized). + /// The projected vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 ProjectOnPlanePreNormalized(this Vector3 vector, Vector3 planeNormal) + { + var dot = Vector3.Dot(vector, planeNormal); + return new Vector3( + vector.x - planeNormal.x * dot, + vector.y - planeNormal.y * dot, + vector.z - planeNormal.z * dot); + } + + /// + /// Overload for Vector3d, returns Vector3. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 ProjectOnPlanePreNormalized(this Vector3d vector, Vector3 planeNormal) + { + var dot = Vector3.Dot(vector, planeNormal); + return new Vector3( + (float)vector.x - planeNormal.x * dot, + (float)vector.y - planeNormal.y * dot, + (float)vector.z - planeNormal.z * dot); + } + + /// + /// Project a vector onto a plane defined by the plane normal (not-necessarily normalized). + /// + /// This implementation is the same as the Unity reference implementation, + /// but with an extra optimisation to reduce the number of division operations to 1. + /// + /// The vector to project. + /// The plane normal. + /// The projected vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 ProjectOnPlane(this Vector3 vector, Vector3 planeNormal) + { + var sqrMag = Vector3.Dot(planeNormal, planeNormal); + if (sqrMag < Mathf.Epsilon) return vector; + var dotNorm = Vector3.Dot(vector, planeNormal) / sqrMag; + return new Vector3( + vector.x - planeNormal.x * dotNorm, + vector.y - planeNormal.y * dotNorm, + vector.z - planeNormal.z * dotNorm); + } + + /// + /// Overload for Vector3d, returns Vector3. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 ProjectOnPlane(this Vector3d vector, Vector3 planeNormal) + { + var sqrMag = Vector3.Dot(planeNormal, planeNormal); + if (sqrMag < Mathf.Epsilon) return vector; + var dotNorm = Vector3.Dot(vector, planeNormal) / sqrMag; + return new Vector3( + (float)vector.x - planeNormal.x * dotNorm, + (float)vector.y - planeNormal.y * dotNorm, + (float)vector.z - planeNormal.z * dotNorm); + } + } +} \ No newline at end of file diff --git a/BDArmory/Extensions/VesselExtensions.cs b/BDArmory/Extensions/VesselExtensions.cs new file mode 100644 index 000000000..02d2678d9 --- /dev/null +++ b/BDArmory/Extensions/VesselExtensions.cs @@ -0,0 +1,211 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using UnityEngine; + +using BDArmory.Weapons.Missiles; +using BDArmory.Utils; + +namespace BDArmory.Extensions +{ + public static class VesselExtensions + { + public static HashSet InOrbitSituations = new HashSet { Vessel.Situations.ORBITING, Vessel.Situations.SUB_ORBITAL, Vessel.Situations.ESCAPING }; + + public static bool InOrbit(this Vessel v) + { + if (v == null) return false; + return InOrbitSituations.Contains(v.situation); + } + + public static bool InVacuum(this Vessel v) + { + return v.atmDensity <= 0.001f; + } + + public static bool IsUnderwater(this Vessel v) + { + if (!v) return false; + return v.altitude < -20; //some boats sit slightly underwater, this is only for submersibles + } + + /// + /// Check for a vessel being a missile. + /// It's considered a missile if the root part is a missile, or it has a MMG that has fired. + /// + public static bool IsMissile(this Vessel v) + { + if (v.rootPart.IsMissile()) return true; + var mmg = VesselModuleRegistry.GetModule(v); + if (mmg == null) return false; + return mmg.HasFired; + } + + /// + /// Get the vessel's velocity accounting for whether it's in orbit and optionally whether it's above 100km (which is another hard-coded KSP limit). + /// + /// + /// + /// + public static Vector3d Velocity(this Vessel v, bool altitudeCheck = true) + { + try + { + if (v == null) return Vector3d.zero; + if (v.InOrbit() && (!altitudeCheck || v.altitude > 1e5f)) return v.obt_velocity; + else return v.srf_velocity; + } + catch (Exception e) + { + Debug.LogWarning("[BDArmory.VesselExtensions]: Exception thrown in Velocity: " + e.Message + "\n" + e.StackTrace); + //return v.srf_velocity; + return new Vector3d(0, 0, 0); + } + } + + public static double GetFutureAltitude(this Vessel vessel, float predictionTime = 10) => GetRadarAltitudeAtPos(AIUtils.PredictPosition(vessel, predictionTime)); + + public static Vector3 GetFuturePosition(this Vessel vessel, float predictionTime = 10) => AIUtils.PredictPosition(vessel, predictionTime); + + public static float GetRadarAltitudeAtPos(Vector3 position) + { + double latitudeAtPos = FlightGlobals.currentMainBody.GetLatitude(position); + double longitudeAtPos = FlightGlobals.currentMainBody.GetLongitude(position); + + float radarAlt = Mathf.Clamp( + (float)(FlightGlobals.currentMainBody.GetAltitude(position) - + FlightGlobals.currentMainBody.TerrainAltitude(latitudeAtPos, longitudeAtPos)), 0, + (float)FlightGlobals.currentMainBody.GetAltitude(position)); + return radarAlt; + } + + // Get a vessel's "radius". + public static float GetRadius(this Vessel vessel, Vector3 fireTransform = default(Vector3), Vector3 bounds = default(Vector3)) + { + if (fireTransform == Vector3.zero || bounds == Vector3.zero) + { + // Get vessel size. + Vector3 size = vessel.vesselSize; + + // Get largest dimension as this is mostly used for terrain/vessel avoidance. More precise "radii" should probably pass the fireTransform and bounds parameters. + return Mathf.Max(Mathf.Max(size.x, size.y), size.z) / 2f; + } + else + { + // Check the 4 diagonals of the box and take the max. + var radius = BDAMath.Sqrt(Mathf.Max( + (vessel.vesselTransform.up * bounds.y + vessel.vesselTransform.right * bounds.x + vessel.vesselTransform.forward * bounds.z).ProjectOnPlane(fireTransform).sqrMagnitude, + (-vessel.vesselTransform.up * bounds.y + vessel.vesselTransform.right * bounds.x + vessel.vesselTransform.forward * bounds.z).ProjectOnPlane(fireTransform).sqrMagnitude, + (vessel.vesselTransform.up * bounds.y - vessel.vesselTransform.right * bounds.x + vessel.vesselTransform.forward * bounds.z).ProjectOnPlane(fireTransform).sqrMagnitude, + (vessel.vesselTransform.up * bounds.y + vessel.vesselTransform.right * bounds.x - vessel.vesselTransform.forward * bounds.z).ProjectOnPlane(fireTransform).sqrMagnitude + )) / 2f; +#if DEBUG + if (radius < bounds.x / 2f && radius < bounds.y / 2f && radius < bounds.z / 2f) Debug.LogWarning($"DEBUG Radius {radius} of {vessel.vesselName} is less than half its minimum bounds {bounds}"); +#endif + return Mathf.Min(radius, (Mathf.Max(Mathf.Max(vessel.vesselSize.x, vessel.vesselSize.y), vessel.vesselSize.z) / 2f) * 1.732f); // clamp bounds to vesselsize in case of Bounds erroneously reporting vessel sizes that are impossibly large + } + } + + static HashSet badBoundsParts = null; + static void GetBadBoundsParts() + { + badBoundsParts = new HashSet(); + foreach (var part in PartLoader.LoadedPartsList) + { + var weapon = part.partPrefab.FindModuleImplementing(); + if (weapon != null) + { + if (!string.IsNullOrEmpty(weapon.name)) // For some reason, the weapons are showing up a second time with an empty name. + Debug.Log($"[BDArmory.VesselExtensions]: Adding {weapon.name} to the bounds exclusion list."); + badBoundsParts.Add(weapon.name); // Exclude all weapons as they can become unreasonably large if they have line renderers attached to them. + } + } + } + + /// + /// Get a vessel's bounds. + /// + /// The vessel to get the bounds of. + /// Use the renderer bounds and calculate min/max manually instead of using KSP's internal functions. + /// + public static Vector3 GetBounds(this Vessel vessel, bool useBounds = true) + { + if (vessel is null || vessel.packed || !vessel.loaded) return Vector3.zero; + if (badBoundsParts == null) GetBadBoundsParts(); + var vesselRot = vessel.transform.rotation; + vessel.SetRotation(Quaternion.identity); + + Vector3 size = Vector3.zero; + Vector3 min = default, max = default; + if (!useBounds) + { + size = ShipConstruction.CalculateCraftSize(vessel.Parts, vessel.rootPart); //x: Width, y: Length, z: Height + } + else + { + var rootBound = GetRendererPartBounds(vessel.rootPart); + min = rootBound.min; max = rootBound.max; + using (var part = vessel.Parts.GetEnumerator()) + while (part.MoveNext()) + { + if (badBoundsParts.Contains(part.Current.name)) continue; // Skip parts that are known to give bad bounds (e.g., lasers when firing). + var partBound = GetRendererPartBounds(part.Current); + min.x = Mathf.Min(min.x, partBound.min.x); + min.y = Mathf.Min(min.y, partBound.min.y); + min.z = Mathf.Min(min.z, partBound.min.z); + max.x = Mathf.Max(max.x, partBound.max.x); + max.y = Mathf.Max(max.y, partBound.max.y); + max.z = Mathf.Max(max.z, partBound.max.z); + } + size = max - min; //x: Width, y: Length, z: Height + } +#if DEBUG + var GetBoundString = (Part p) => { var bwop = GetRendererPartBounds(p); return $"{bwop.size}@{bwop.center}"; }; + if (size.x > 1000 || size.y > 1000 || size.z > 1000) Debug.LogWarning($"DEBUG Bounds on {vessel.vesselName} are bad: {size} (max: {max}, min: {min}, useBounds: {useBounds}). Parts: {string.Join("; ", vessel.Parts.Select(p => $"{p.name}, collider bounds: {string.Join(", ", p.GetColliderBounds().Select(b => $"{b.size}@{b.center}"))}, bounds w/o particles: {GetBoundString(p)}"))}. Root: {vessel.rootPart.name}, bounds {string.Join(", ", vessel.rootPart.GetColliderBounds().Select(b => $"{b.size}@{b.center}"))}."); +#endif + vessel.SetRotation(vesselRot); + return size; + } + + /// + /// Work-around for pre-1.11 versions of KSP not having GameObject.GetRendererBoundsWithoutParticles(). + /// + /// + /// + static Bounds GetRendererPartBounds(Part part) + { + if ((Versioning.version_major == 1 && Versioning.version_minor > 10) || Versioning.version_major > 1) // Introduced in 1.11 + return GetRendererPartBounds_1_11(part); + else + return part.gameObject.GetRendererBounds(); + } + + static Bounds GetRendererPartBounds_1_11(Part part) + { + return part.gameObject.GetRendererBoundsWithoutParticles(); + } + + /// + /// Work-around for pre-1.11 versions of KSP not having Vessel.FindVesselModuleImplementing(). + /// + /// + /// + /// + public static T FindVesselModuleImplementingBDA(this Vessel vessel) where T : class + { + if ((Versioning.version_major == 1 && Versioning.version_minor > 10) || Versioning.version_major > 1) // Introduced in 1.11 + return vessel.FindVesselModuleImplementing_1_11(); + else + { + foreach (var module in vessel.vesselModules) + if (module is T) + return module as T; + return null; + } + } + static T FindVesselModuleImplementing_1_11(this Vessel vessel) where T : class + { + return vessel.FindVesselModuleImplementing(); + } + } +} diff --git a/BDArmory/FX/BDAGaplessParticleEmitter.cs b/BDArmory/FX/BDAGaplessParticleEmitter.cs index c5d7c72f1..a4440e4d0 100644 --- a/BDArmory/FX/BDAGaplessParticleEmitter.cs +++ b/BDArmory/FX/BDAGaplessParticleEmitter.cs @@ -1,6 +1,8 @@ -using BDArmory.UI; using UnityEngine; +using BDArmory.Settings; +using BDArmory.UI; + namespace BDArmory.FX { public class BDAGaplessParticleEmitter : MonoBehaviour @@ -53,6 +55,8 @@ void OnEnable() void FixedUpdate() { + if (!BDArmorySettings.GAPLESS_PARTICLE_EMITTERS) return; + if (!part && !rb) { internalVelocity = (transform.position - lastPos) / Time.fixedDeltaTime; @@ -98,6 +102,8 @@ void FixedUpdate() public void EmitParticles() { + if (!BDArmorySettings.GAPLESS_PARTICLE_EMITTERS) return; + Vector3 originalLocalPosition = gameObject.transform.localPosition; Vector3 originalPosition = gameObject.transform.position; Vector3 startPosition = gameObject.transform.position + (velocity * Time.fixedDeltaTime); diff --git a/BDArmory/FX/BulletHitFX.cs b/BDArmory/FX/BulletHitFX.cs index f642e6626..c82291628 100644 --- a/BDArmory/FX/BulletHitFX.cs +++ b/BDArmory/FX/BulletHitFX.cs @@ -1,21 +1,26 @@ using System.Collections.Generic; -using BDArmory.Core; -using BDArmory.Core.Module; -using BDArmory.Core.Extension; -using BDArmory.Misc; -using BDArmory.UI; -using BDArmory.Modules; using UniLinq; using UnityEngine; +using BDArmory.Damage; +using BDArmory.Extensions; +using BDArmory.Settings; +using BDArmory.UI; +using BDArmory.Utils; + namespace BDArmory.FX { [KSPAddon(KSPAddon.Startup.Flight, false)] class Decal : MonoBehaviour { Part parentPart; + // string parentPartName = ""; + // string parentVesselName = ""; + static bool hasOnVesselUnloaded = false; public static ObjectPool CreateDecalPool(string modelPath) { + if ((Versioning.version_major == 1 && Versioning.version_minor > 10) || Versioning.version_major > 1) // onVesselUnloaded event introduced in 1.11 + hasOnVesselUnloaded = true; var template = GameDatabase.Instance.GetModel(modelPath); var decal = template.AddComponent(); template.AddOrGetComponent(); @@ -25,12 +30,20 @@ public static ObjectPool CreateDecalPool(string modelPath) public void AttachAt(Part hitPart, RaycastHit hit, Vector3 offset) { + if (hitPart is null) return; parentPart = hitPart; + // parentPartName = parentPart.name; + // parentVesselName = parentPart.vessel.vesselName; transform.SetParent(hitPart.transform); transform.position = hit.point + offset; transform.rotation = Quaternion.FromToRotation(Vector3.forward, hit.normal); parentPart.OnJustAboutToDie += OnParentDestroy; parentPart.OnJustAboutToBeDestroyed += OnParentDestroy; + if (hasOnVesselUnloaded) + { + OnVesselUnloaded_1_11(false); // Remove any previous onVesselUnloaded event handler (due to forced reuse in the pool). + OnVesselUnloaded_1_11(true); // Catch unloading events too. + } gameObject.SetActive(true); } public void SetColor(Color color) @@ -48,21 +61,54 @@ public void SetColor(Color color) } } - public void OnParentDestroy() + + void OnParentDestroy() { - if (parentPart) + if (parentPart is not null) { parentPart.OnJustAboutToDie -= OnParentDestroy; parentPart.OnJustAboutToBeDestroyed -= OnParentDestroy; + Deactivate(); + } + } + + void OnVesselUnloaded(Vessel vessel) + { + if (parentPart is not null && (parentPart.vessel is null || parentPart.vessel == vessel)) + { + OnParentDestroy(); + } + else if (parentPart is null) + { + Deactivate(); + } + } + + void OnVesselUnloaded_1_11(bool addRemove) // onVesselUnloaded event introduced in 1.11 + { + if (addRemove) + GameEvents.onVesselUnloaded.Add(OnVesselUnloaded); + else + GameEvents.onVesselUnloaded.Remove(OnVesselUnloaded); + } + + void Deactivate() + { + if (hasOnVesselUnloaded) // onVesselUnloaded event introduced in 1.11 + OnVesselUnloaded_1_11(false); + if (gameObject is not null && gameObject.activeSelf) // Deactivate even if a parent is already inactive. + { parentPart = null; transform.parent = null; gameObject.SetActive(false); } } - public void OnDestroy() + public void OnDestroy() // This shouldn't be happening except on exiting KSP, but sometimes they get destroyed instead of disabled! { - OnParentDestroy(); // Make sure it's disabled and book-keeping is done. + // if (HighLogic.LoadedSceneIsFlight) Debug.LogError($"[BDArmory.BulletHitFX]: BulletHitFX on {parentPartName} ({parentVesselName}) was destroyed!"); + if (hasOnVesselUnloaded) // onVesselUnloaded event introduced in 1.11 + OnVesselUnloaded_1_11(false); } } @@ -279,13 +325,13 @@ void Awake() if (audioClips == null) { audioClips = new Dictionary{ - {AudioClipType.Ricochet1, GameDatabase.Instance.GetAudioClip("BDArmory/Sounds/ricochet1")}, - {AudioClipType.Ricochet2, GameDatabase.Instance.GetAudioClip("BDArmory/Sounds/ricochet1")}, - {AudioClipType.Ricochet3, GameDatabase.Instance.GetAudioClip("BDArmory/Sounds/ricochet3")}, - {AudioClipType.BulletHit1, GameDatabase.Instance.GetAudioClip("BDArmory/Sounds/bulletHit1")}, - {AudioClipType.BulletHit2, GameDatabase.Instance.GetAudioClip("BDArmory/Sounds/bulletHit2")}, - {AudioClipType.BulletHit3, GameDatabase.Instance.GetAudioClip("BDArmory/Sounds/bulletHit3")}, - {AudioClipType.Artillery_Shot, GameDatabase.Instance.GetAudioClip("BDArmory/Sounds/Artillery_Shot")}, + {AudioClipType.Ricochet1, SoundUtils.GetAudioClip("BDArmory/Sounds/ricochet1")}, + {AudioClipType.Ricochet2, SoundUtils.GetAudioClip("BDArmory/Sounds/ricochet1")}, + {AudioClipType.Ricochet3, SoundUtils.GetAudioClip("BDArmory/Sounds/ricochet3")}, + {AudioClipType.BulletHit1, SoundUtils.GetAudioClip("BDArmory/Sounds/bulletHit1")}, + {AudioClipType.BulletHit2, SoundUtils.GetAudioClip("BDArmory/Sounds/bulletHit2")}, + {AudioClipType.BulletHit3, SoundUtils.GetAudioClip("BDArmory/Sounds/bulletHit3")}, + {AudioClipType.Artillery_Shot, SoundUtils.GetAudioClip("BDArmory/Sounds/Artillery_Shot")}, }; } } @@ -454,7 +500,7 @@ public static void AttachLeak(RaycastHit hit, Part hitPart, float caliber, bool existingLeakFX.lifeTime = 0; //kill leakFX, start fire leakcount++; } - //Debug.Log("[BullethitFX] Adding fire. HE? " + explosive + "; Inc? " + incendiary + "; inerttank? " + inertTank); + //if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log("[BDArmory.BullethitFX]: Adding fire. HE? " + explosive + "; Inc? " + incendiary + "; inerttank? " + inertTank); AttachFire(hit.point, hitPart, caliber, sourcevessel, -1, leakcount); } } @@ -472,7 +518,7 @@ public static void AttachLeak(RaycastHit hit, Part hitPart, float caliber, bool leakFX.lifeTime = (10 * BDArmorySettings.BD_TANK_LEAK_TIME); } } - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BulletHitFX]: BulletHit attaching fuel leak, drainrate: " + leakFX.drainRate); + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log("[BDArmory.BulletHitFX]: BulletHit attaching fuel leak, drainrate: " + leakFX.drainRate); fuelLeak.SetActive(true); } @@ -491,7 +537,7 @@ public static void AttachFire(Vector3 hit, Part hitPart, float caliber, string s fireFX.surfaceFire = surfaceFire; //fireFX.transform.localScale = Vector3.one * (caliber/10); - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BulletHitFX]: BulletHit fire, burn rate: " + fireFX.burnRate + "; Surface fire: " + surfaceFire); + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log("[BDArmory.BulletHitFX]: BulletHit fire, burn rate: " + fireFX.burnRate + "; Surface fire: " + surfaceFire); fire.SetActive(true); } } diff --git a/BDArmory/FX/CameraBulletRenderer.cs b/BDArmory/FX/CameraBulletRenderer.cs index 61401d09e..8592680bb 100644 --- a/BDArmory/FX/CameraBulletRenderer.cs +++ b/BDArmory/FX/CameraBulletRenderer.cs @@ -1,7 +1,8 @@ -using BDArmory.Bullets; -using BDArmory.Modules; using UnityEngine; +using BDArmory.Bullets; +using BDArmory.Weapons; + namespace BDArmory.FX { public class CameraBulletRenderer : MonoBehaviour diff --git a/BDArmory/FX/DecalGaplessParticleEmitter.cs b/BDArmory/FX/DecalGaplessParticleEmitter.cs index 061d054a0..4ff615417 100644 --- a/BDArmory/FX/DecalGaplessParticleEmitter.cs +++ b/BDArmory/FX/DecalGaplessParticleEmitter.cs @@ -1,4 +1,5 @@ using UnityEngine; +using BDArmory.Settings; namespace BDArmory.FX { @@ -51,6 +52,8 @@ void OnEnable() private void FixedUpdate() { + if (!BDArmorySettings.GAPLESS_PARTICLE_EMITTERS) return; + if (!part && !rb) { internalVelocity = (transform.position - lastPos) / Time.fixedDeltaTime; @@ -87,6 +90,8 @@ private void FixedUpdate() public void EmitParticles() { + if (!BDArmorySettings.GAPLESS_PARTICLE_EMITTERS) return; + var partRB = part != null ? part.GetComponent() : null; var velocity = partRB != null ? partRB.velocity : rb.velocity; var originalLocalPosition = gameObject.transform.localPosition; diff --git a/BDArmory/FX/ExplosionFX.cs b/BDArmory/FX/ExplosionFX.cs index 5e268ff65..4ae70a4d7 100644 --- a/BDArmory/FX/ExplosionFX.cs +++ b/BDArmory/FX/ExplosionFX.cs @@ -3,28 +3,31 @@ using System.Linq; using UnityEngine; +using BDArmory.Armor; using BDArmory.Competition; -using BDArmory.Core; -using BDArmory.Core.Extension; -using BDArmory.Core.Module; -using BDArmory.Core.Utils; +using BDArmory.Damage; +using BDArmory.Extensions; using BDArmory.GameModes; -using BDArmory.Misc; -using BDArmory.Modules; -using BDArmory.UI; +using BDArmory.Settings; +using BDArmory.Utils; +using BDArmory.Weapons; namespace BDArmory.FX { public class ExplosionFx : MonoBehaviour { public static Dictionary explosionFXPools = new Dictionary(); + public static Dictionary audioClips = new Dictionary(); // Pool the audio clips separately. Note: this is really a shallow copy of the AudioClips in SoundUtils, but with invalid AudioClips replaced by the default explosion AudioClip. public KSPParticleEmitter[] pEmitters { get; set; } public Light LightFx { get; set; } public float StartTime { get; set; } - public AudioClip ExSound { get; set; } + // public string ExSound { get; set; } + public string SoundPath { get; set; } public AudioSource audioSource { get; set; } private float MaxTime { get; set; } public float Range { get; set; } + public float SCRange { get; set; } + public float penetration { get; set; } public float Caliber { get; set; } public float ProjMass { get; set; } public ExplosionSourceType ExplosionSource { get; set; } @@ -33,19 +36,21 @@ public class ExplosionFx : MonoBehaviour public float Power { get; set; } public Vector3 Position { get; set; } public Vector3 Direction { get; set; } - public float AngleOfEffect { get; set; } + public float cosAngleOfEffect { get; set; } public Part ExplosivePart { get; set; } public bool isFX { get; set; } public float CASEClamp { get; set; } public float dmgMult { get; set; } + public float apMod { get; set; } + public float travelDistance { get; set; } - public Part hitpart { get; set; } + public Part projectileHitPart { get; set; } public float TimeIndex => Time.time - StartTime; private bool disabled = true; float blastRange; - int explosionLayerMask = (int)(LayerMasks.Parts | LayerMasks.Scenery | LayerMasks.EVA | LayerMasks.Unknown19 | LayerMasks.Unknown23); // Why 19 and 23? + const int explosionLayerMask = (int)(LayerMasks.Parts | LayerMasks.Scenery | LayerMasks.EVA | LayerMasks.Unknown19 | LayerMasks.Unknown23 | LayerMasks.Wheels); // Why 19 and 23? Queue explosionEvents = new Queue(); List explosionEventsPreProcessing = new List(); @@ -56,6 +61,9 @@ public class ExplosionFx : MonoBehaviour static RaycastHit[] lineOfSightHits; static RaycastHit[] reverseHits; + static RaycastHit[] sortedLoSHits; + static RaycastHit[] shapedChargeHits; + static RaycastHit miss = new RaycastHit(); static Collider[] overlapSphereColliders; public static List IgnoreParts; public static List IgnoreBuildings; @@ -72,10 +80,14 @@ public enum WarheadTypes public WarheadTypes warheadType; + static List> LoSIntermediateParts = new List>(); // Worker list for LoS checks to avoid reallocations. + void Awake() { if (lineOfSightHits == null) { lineOfSightHits = new RaycastHit[100]; } if (reverseHits == null) { reverseHits = new RaycastHit[100]; } + if (sortedLoSHits == null) { sortedLoSHits = new RaycastHit[100]; } + if (shapedChargeHits == null) { shapedChargeHits = new RaycastHit[100]; } if (overlapSphereColliders == null) { overlapSphereColliders = new Collider[1000]; } if (IgnoreParts == null) { IgnoreParts = new List(); } if (IgnoreBuildings == null) { IgnoreBuildings = new List(); } @@ -85,7 +97,7 @@ private void OnEnable() { StartTime = Time.time; disabled = false; - MaxTime = Mathf.Sqrt((Range / ExplosionVelocity) * 3f) * 2f; // Scale MaxTime to get a reasonable visualisation of the explosion. + MaxTime = BDAMath.Sqrt((Range / ExplosionVelocity) * 3f) * 2f; // Scale MaxTime to get a reasonable visualisation of the explosion. blastRange = warheadType == WarheadTypes.Standard ? Range * 2 : Range; //to properly account for shrapnel hits when compiling list of hit parts from the spherecast if (!isFX) { @@ -105,13 +117,29 @@ private void OnEnable() LightFx = gameObject.GetComponent(); LightFx.range = Range * 3f; - - if (BDArmorySettings.DRAW_DEBUG_LABELS) + LightFx.intensity = 8f; // Reset light intensity. + + audioSource = gameObject.GetComponent(); + // if (ExSound == null) + // { + // ExSound = SoundUtils.GetAudioClip(SoundPath); + + // if (ExSound == null) + // { + // Debug.LogError("[BDArmory.ExplosionFX]: " + SoundPath + " was not found, using the default sound instead. Please fix your model."); + // ExSound = SoundUtils.GetAudioClip(ModuleWeapon.defaultExplSoundPath); + // } + // } + if (!string.IsNullOrEmpty(SoundPath)) + { + audioSource.PlayOneShot(audioClips[SoundPath]); + } + if (BDArmorySettings.DEBUG_DAMAGE) { Debug.Log("[BDArmory.ExplosionFX]: Explosion started tntMass: {" + Power + "} BlastRadius: {" + Range + "} StartTime: {" + StartTime + "}, Duration: {" + MaxTime + "}"); } /* - if (BDArmorySettings.PERSISTENT_FX && Caliber > 30 && Utils.GetRadarAltitudeAtPos(transform.position) > Caliber / 60) + if (BDArmorySettings.PERSISTENT_FX && Caliber > 30 && BodyUtils.GetRadarAltitudeAtPos(transform.position) > Caliber / 60) { if (FlightGlobals.getAltitudeAtPos(transform.position) > Caliber / 60) { @@ -149,7 +177,7 @@ private void CalculateBlastEvents() { if (enuEvents.Current == null) continue; - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_DAMAGE) { Debug.Log("[BDArmory.ExplosionFX]: Enqueueing Blast Event"); } @@ -172,7 +200,7 @@ private List ProcessingBlastSphere() switch (ExplosionSource) { case ExplosionSourceType.Missile: - var explosivePart = ExplosivePart ? ExplosivePart.FindModuleImplementing() : null; + var explosivePart = ExplosivePart ? ExplosivePart.FindModuleImplementing() : null; //isn't this already set in the explosion setup? sourceVesselName = explosivePart ? explosivePart.sourcevessel.GetName() : SourceVesselName; break; default: // Everything else. @@ -180,14 +208,41 @@ private List ProcessingBlastSphere() break; } } + SCRange = 0; if (warheadType == WarheadTypes.ShapedCharge) { - Ray SCRay = new Ray(Position, (Direction.normalized * Range)); - var hits = Physics.RaycastAll(SCRay, Range, explosionLayerMask); - if (BDArmorySettings.DRAW_ARMOR_LABELS) Debug.Log("[ExplosionFX] SC plasmaJet raycast hits: " + hits.Length); - if (hits.Length > 0) + // Based on shaped charge standoff penetration falloff, set equal to 10% and solved for the range + // Equation is from https://www.diva-portal.org/smash/get/diva2:643824/FULLTEXT01.pdf and gives an + // answer in the same units as caliber, thus we divide by 1000 to get the range in meters. The long + // number is actually 2*sqrt(19), however for speed this has been pre-calculated and rounded to 8 sig + // figs behind the decimal point and turned into a floating point number (which in theory should drop it + // to 8 sig figs and should be indistinguishable from if we had actually calculated it at runtime). We then + // use this range to raycast those hits if it is greater than Range. This will currently overpredict for + // small missiles and underpredict for large ones since they don't have a caliber associated with them + // and as such will use 120 mm by default (since caliber == 0, thus it'll take 6f as the jet size which + // corresponds to a 120 mm charge. Perhaps think about including a caliber field? + //SCRange = (7f * (8.71779789f * 20f * Caliber + 20f * Caliber))* 0.001f; // 5% + // Decided to swap it to 10% since 5% gave pretty big ranges on the order of several meters and 10% actually + // simplifies down to a linear equation + SCRange = (49f * Caliber * 20f) * 0.001f; + + //if (BDArmorySettings.DEBUG_WEAPONS && (warheadType == WarheadTypes.ShapedCharge)) + //{ + // Debug.Log("[BDArmory.ExplosionFX] SCRange: " + SCRange + "m. Normalized Direction: " + Direction.normalized.ToString("G4")); + //} + + Ray SCRay = new Ray(Position, Direction); + //Ray SCRay = new Ray(Position, (Direction.normalized * Range)); + var hitCount = Physics.RaycastNonAlloc(SCRay, shapedChargeHits, SCRange > Range ? SCRange : Range, explosionLayerMask); + if (hitCount == shapedChargeHits.Length) // If there's a whole bunch of stuff in the way (unlikely), then we need to increase the size of our hits buffer. { - var orderedHits = hits.OrderBy(x => x.distance); + shapedChargeHits = Physics.RaycastAll(SCRay, SCRange > Range ? SCRange : Range, explosionLayerMask); + hitCount = shapedChargeHits.Length; + } + if (BDArmorySettings.DEBUG_ARMOR) Debug.Log($"[BDArmory.ExplosionFX]: SC plasmaJet raycast hits: {hitCount}"); + if (hitCount > 0) + { + var orderedHits = shapedChargeHits.Take(hitCount).OrderBy(x => x.distance); using (var hitsEnu = orderedHits.GetEnumerator()) { @@ -204,7 +259,7 @@ private List ProcessingBlastSphere() if (hitPart.vessel.GetName() == SourceVesselName) continue; //avoid autohit; if (hitPart.mass > 0 && !explosionEventsPartsAdded.Contains(hitPart)) { - var damaged = ProcessPartEvent(hitPart, sourceVesselName, explosionEventsPreProcessing, explosionEventsPartsAdded, true); + var damaged = ProcessPartEvent(hitPart, SChit.distance, SourceVesselName, explosionEventsPreProcessing, explosionEventsPartsAdded, true); // If the explosion derives from a missile explosion, count the parts damaged for missile hit scores. if (damaged && BDACompetitionMode.Instance) { @@ -233,13 +288,12 @@ private List ProcessingBlastSphere() } else { - DestructibleBuilding building = SChit.collider.gameObject.GetComponentUpwards(); - - if (building != null) + if (!BDArmorySettings.PAINTBALL_MODE) { - if (!explosionEventsBuildingAdded.Contains(building)) + DestructibleBuilding building = SChit.collider.gameObject.GetComponentUpwards(); + if (building != null) { - ProcessBuildingEvent(building, explosionEventsPreProcessing, explosionEventsBuildingAdded); + ProjectileUtils.CheckBuildingHit(SChit, Power * 0.0555f, Direction.normalized * 4000f, 1); } } } @@ -253,79 +307,112 @@ private List ProcessingBlastSphere() overlapSphereColliders = Physics.OverlapSphere(Position, blastRange, explosionLayerMask); overlapSphereColliderCount = overlapSphereColliders.Length; } - using (var hitCollidersEnu = overlapSphereColliders.Take(overlapSphereColliderCount).ToList().GetEnumerator()) + using (var hitCollidersEnu = overlapSphereColliders.Take(overlapSphereColliderCount).GetEnumerator()) { while (hitCollidersEnu.MoveNext()) { if (hitCollidersEnu.Current == null) continue; - - Part partHit = hitCollidersEnu.Current.GetComponentInParent(); - if (partHit == null) continue; - - if (partHit != null) + try { - if (ProjectileUtils.IsIgnoredPart(partHit)) continue; // Ignore ignored parts. - if (partHit.mass > 0 && !explosionEventsPartsAdded.Contains(partHit)) + Part partHit = hitCollidersEnu.Current.gameObject.GetComponentInParent(); + if (partHit != null) { - var damaged = ProcessPartEvent(partHit, sourceVesselName, explosionEventsPreProcessing, explosionEventsPartsAdded); - // If the explosion derives from a missile explosion, count the parts damaged for missile hit scores. - if (damaged && BDACompetitionMode.Instance) - { - bool registered = false; - var damagedVesselName = partHit.vessel != null ? partHit.vessel.GetName() : null; - switch (ExplosionSource) - { - case ExplosionSourceType.Rocket: - if (BDACompetitionMode.Instance.Scores.RegisterRocketHit(sourceVesselName, damagedVesselName, 1)) - registered = true; - break; - case ExplosionSourceType.Missile: - if (BDACompetitionMode.Instance.Scores.RegisterMissileHit(sourceVesselName, damagedVesselName, 1)) - registered = true; - break; - } - if (registered) + if (ProjectileUtils.IsIgnoredPart(partHit)) continue; // Ignore ignored parts. + if (ExplosivePart != null && partHit.name == ExplosivePart.name) continue; //don't fratricide fellow missiles/bombs in a launched salvo when the first detonates + if (partHit.mass > 0 && !explosionEventsPartsAdded.Contains(partHit)) + { + var damaged = ProcessPartEvent(partHit, Vector3.Distance(hitCollidersEnu.Current.ClosestPoint(Position), Position), sourceVesselName, explosionEventsPreProcessing, explosionEventsPartsAdded); + // If the explosion derives from a missile explosion, count the parts damaged for missile hit scores. + if (damaged && BDACompetitionMode.Instance) { - if (explosionEventsVesselsHit.ContainsKey(damagedVesselName)) - ++explosionEventsVesselsHit[damagedVesselName]; - else - explosionEventsVesselsHit[damagedVesselName] = 1; + bool registered = false; + + var damagedVesselName = partHit.vessel != null ? partHit.vessel.GetName() : null; + switch (ExplosionSource) + { + case ExplosionSourceType.Rocket: + if (BDACompetitionMode.Instance.Scores.RegisterRocketHit(sourceVesselName, damagedVesselName, 1)) + registered = true; + break; + case ExplosionSourceType.Missile: + if (BDACompetitionMode.Instance.Scores.RegisterMissileHit(sourceVesselName, damagedVesselName, 1)) + registered = true; + break; + case ExplosionSourceType.Bullet: + if (travelDistance > 0) + registered = true; + break; + } + if (registered) + { + if (explosionEventsVesselsHit.ContainsKey(damagedVesselName)) + ++explosionEventsVesselsHit[damagedVesselName]; + else + explosionEventsVesselsHit[damagedVesselName] = 1; + } } } } - } - else - { - DestructibleBuilding building = hitCollidersEnu.Current.GetComponentInParent(); - - if (building != null) + else { - if (!explosionEventsBuildingAdded.Contains(building)) + DestructibleBuilding building = hitCollidersEnu.Current.GetComponentInParent(); + + if (building != null) { - ProcessBuildingEvent(building, explosionEventsPreProcessing, explosionEventsBuildingAdded); + if (!explosionEventsBuildingAdded.Contains(building)) + { + //ProcessBuildingEvent(building, explosionEventsPreProcessing, explosionEventsBuildingAdded); + Ray ray = new Ray(Position, building.transform.position - Position); + var distance = Vector3.Distance(building.transform.position, Position); + RaycastHit rayHit; + if (Physics.Raycast(ray, out rayHit, Range * 2, explosionLayerMask)) + { + //DestructibleBuilding destructibleBuilding = rayHit.collider.gameObject.GetComponentUpwards(); + distance = Vector3.Distance(Position, rayHit.point); + //if (destructibleBuilding != null && destructibleBuilding.Equals(building) && building.IsIntact) + if (building.IsIntact) + { + explosionEventsPreProcessing.Add(new BuildingBlastHitEvent() { Distance = distance, Building = building, TimeToImpact = distance / ExplosionVelocity }); + explosionEventsBuildingAdded.Add(building); + } + } + } } } } + catch (Exception e) + { + Debug.LogError($"[BDArmory.ExplosionFX]: Exception in overlapSphereColliders processing: {e.Message}\n{e.StackTrace}"); + } } } if (explosionEventsVesselsHit.Count > 0) { - if (ExplosionSource != ExplosionSourceType.Rocket) // Bullet explosions aren't registered in explosionEventsVesselsHit. - { - string message = ""; - foreach (var vesselName in explosionEventsVesselsHit.Keys) - message += (message == "" ? "" : " and ") + vesselName + " had " + explosionEventsVesselsHit[vesselName]; - if (ExplosionSource == ExplosionSourceType.Missile) - { - message += " parts damaged due to missile strike"; - } - else //ExplosionType BattleDamage || Other + string message = ""; + foreach (var vesselName in explosionEventsVesselsHit.Keys) + //message += (message == "" ? "" : " and ") + vesselName + " had " + explosionEventsVesselsHit[vesselName]; + switch (ExplosionSource) { - message += " parts damaged due to explosion"; + case ExplosionSourceType.Missile: + message += (message == "" ? "" : " and ") + vesselName + " had " + explosionEventsVesselsHit[vesselName]; + message += " parts damaged due to missile strike"; + message += (SourceWeaponName != null ? $" ({SourceWeaponName})" : "") + (sourceVesselName != null ? $" from {sourceVesselName}" : "") + "."; + break; + case ExplosionSourceType.Bullet: + message += (message == "" ? "" : " and ") + vesselName + " had " + explosionEventsVesselsHit[vesselName] + " parts damaged from"; + message += (sourceVesselName != null ? $" from {sourceVesselName}'s" : "") + (SourceWeaponName != null ? $" ({SourceWeaponName})" : "shell hit") + ($" at {travelDistance:F3}m") + "."; + break; + case ExplosionSourceType.Rocket: + { + if (travelDistance > 0) + { + message += (message == "" ? "" : " and ") + vesselName + " had " + explosionEventsVesselsHit[vesselName] + " parts damaged from"; + message += (sourceVesselName != null ? $" from {sourceVesselName}'s" : "") + (SourceWeaponName != null ? $" ({SourceWeaponName})" : "rocket hit") + ($" at {travelDistance:F3}m") + "."; + } + break; + } } - message += (SourceWeaponName != null ? " (" + SourceWeaponName + ")" : "") + (sourceVesselName != null ? " from " + sourceVesselName : "") + "."; - BDACompetitionMode.Instance.competitionStatus.Add(message); - } + if (!String.IsNullOrEmpty(message)) BDACompetitionMode.Instance.competitionStatus.Add(message); // Note: damage hasn't actually been applied to the parts yet, just assigned as events, so we can't know if they survived. foreach (var vesselName in explosionEventsVesselsHit.Keys) // Note: sourceVesselName is already checked for being in the competition before damagedVesselName is added to explosionEventsVesselsHitByMissiles, so we don't need to check it here. { @@ -359,21 +446,21 @@ private void ProcessBuildingEvent(DestructibleBuilding building, List eventList, List partsAdded, bool angleOverride = false) + private bool ProcessPartEvent(Part part, float hitDist, string sourceVesselName, List eventList, List partsAdded, bool angleOverride = false) { RaycastHit hit; - float distance = 0; - List> intermediateParts; - if (IsInLineOfSight(part, ExplosivePart, out hit, out distance, out intermediateParts)) + float distance; + if (IsInLineOfSight(part, ExplosivePart, hitDist, out hit, out distance)) { //if (IsAngleAllowed(Direction, hit)) //{ //Adding damage hit - if (distance <= Range)//part within blast + if (distance <= (blastRange > SCRange ? blastRange : SCRange))//part within total range of shrapnel + blast? { eventList.Add(new PartBlastHitEvent() { @@ -383,15 +470,12 @@ private bool ProcessPartEvent(Part part, string sourceVesselName, List 0 && distance <= blastRange) //maybe move this to ExecutePartBlastHitEvent so shrap hits aren't instantaneous - { - ProjectileUtils.CalculateShrapnelDamage(part, hit, Caliber, Power, distance, sourceVesselName, ExplosionSource, ProjMass); //part hit by shrapnel, but not pressure wave - } partsAdded.Add(part); + return true; //} } @@ -402,13 +486,14 @@ private bool IsAngleAllowed(Vector3 direction, RaycastHit hit, Part p) { if (direction == default(Vector3)) { - //Debug.Log("[ExplosionFX] Default Direction param! " + p.name + " angle from explosion dir irrelevant!"); + //if (BDArmorySettings.DEBUG_LABELS) Debug.Log("[BDArmory.ExplosionFX]: Default Direction param! " + p.name + " angle from explosion dir irrelevant!"); return true; } if (warheadType == WarheadTypes.ContinuousRod) { - Debug.Log("[ExplosionFX] " + p.name + " at " + Vector3.Angle(direction, (hit.point - Position).normalized) + " angle from CR explosion direction"); - if (Vector3.Angle(direction, (hit.point - Position).normalized) >= 75 && Vector3.Angle(direction, (hit.point - Position).normalized) <= 105) + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log($"[BDArmory.ExplosionFX]: {p.name} at {Vector3.Angle(direction, (hit.point - Position).normalized)} angle from CR explosion direction"); + //if (Vector3.Angle(direction, (hit.point - Position).normalized) >= 60 && Vector3.Angle(direction, (hit.point - Position).normalized) <= 90) + if (Vector3.Dot(direction, (hit.point - Position).normalized) <= 0.5 && Vector3.Dot(direction, (hit.point - Position).normalized) >= 0) { return true; } @@ -416,8 +501,8 @@ private bool IsAngleAllowed(Vector3 direction, RaycastHit hit, Part p) } else { - Debug.Log("[ExplosionFX] " + p.name + " at " + Vector3.Angle(direction, (hit.point - Position).normalized) + $" angle from {warheadType} explosion direction"); - return (Vector3.Angle(direction, (hit.point - Position).normalized) <= AngleOfEffect); + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log($"[BDArmory.ExplosionFX]: {p.name} at {Vector3.Angle(direction, (hit.point - Position).normalized)} angle from {warheadType} explosion direction"); + return (Vector3.Dot(direction, (hit.point - Position).normalized) >= cosAngleOfEffect); } } @@ -427,54 +512,94 @@ private bool IsAngleAllowed(Vector3 direction, RaycastHit hit, Part p) /// /// /// - /// out property with the actual hit + /// The raycast hit + /// The distance of the hit + /// Update the LoSIntermediateParts list /// - private bool IsInLineOfSight(Part part, Part explosivePart, out RaycastHit hit, out float distance, out List> intermediateParts) + private bool IsInLineOfSight(Part part, Part explosivePart, float startDist, out RaycastHit hit, out float distance, bool intermediateParts = true) { - Ray partRay = new Ray(Position, part.transform.position - Position); + var partPosition = part.transform.position; //transition over to part.Collider.ClosestPoint(Position);? Test later + Ray partRay = new Ray(Position, partPosition - Position); + float range = blastRange > SCRange ? blastRange : SCRange; - var hitCount = Physics.RaycastNonAlloc(partRay, lineOfSightHits, blastRange, explosionLayerMask); + var hitCount = Physics.RaycastNonAlloc(partRay, lineOfSightHits, range, explosionLayerMask); if (hitCount == lineOfSightHits.Length) // If there's a whole bunch of stuff in the way (unlikely), then we need to increase the size of our hits buffer. { - lineOfSightHits = Physics.RaycastAll(partRay, blastRange, explosionLayerMask); + lineOfSightHits = Physics.RaycastAll(partRay, range, explosionLayerMask); hitCount = lineOfSightHits.Length; } - int reverseHitCount = 0; //check if explosion is originating inside a part - reverseHitCount = Physics.RaycastNonAlloc(new Ray(part.transform.position - Position, Position), reverseHits, blastRange, explosionLayerMask); + Ray reverseRay = new Ray(partRay.origin + range * partRay.direction, -partRay.direction); + int reverseHitCount = Physics.RaycastNonAlloc(reverseRay, reverseHits, range, explosionLayerMask); if (reverseHitCount == reverseHits.Length) { - reverseHits = Physics.RaycastAll(new Ray(part.transform.position - Position, Position), blastRange, explosionLayerMask); + reverseHits = Physics.RaycastAll(reverseRay, range, explosionLayerMask); reverseHitCount = reverseHits.Length; } for (int i = 0; i < reverseHitCount; ++i) - { reverseHits[i].distance = blastRange - reverseHits[i].distance; } + { + reverseHits[i].distance = range - reverseHits[i].distance; + reverseHits[i].normal = -reverseHits[i].normal; + } - intermediateParts = new List>(); + LoSIntermediateParts.Clear(); + var totalHitCount = CollateHits(ref lineOfSightHits, hitCount, ref reverseHits, reverseHitCount); // This is the most expensive part of this method and the cause of most of the slow-downs with explosions. + float factor = 1.0f; + for (int i = 0; i < totalHitCount; ++i) + { + hit = sortedLoSHits[i]; + Part partHit = hit.collider.GetComponentInParent(); + if (partHit == null) continue; + if (ProjectileUtils.IsIgnoredPart(partHit)) continue; // Ignore ignored parts. + //if (startDist > -100) + //{ + if (partHit == projectileHitPart) distance = 0.05f; //HE bullet slamming into armor/penning and detonating inside part + else distance = Mathf.Max(startDist, 0.05f); + //} + //if (startDist < 0) distance = hit.distance; - using (var hitsEnu = lineOfSightHits.Take(hitCount).Concat(reverseHits.Take(reverseHitCount)).OrderBy(x => x.distance).GetEnumerator()) - while (hitsEnu.MoveNext()) + if (partHit == part) + { + return true; + } + if (partHit != part) { - Part partHit = hitsEnu.Current.collider.GetComponentInParent(); - if (partHit == null) continue; - if (ProjectileUtils.IsIgnoredPart(partHit)) continue; // Ignore ignored parts. - hit = hitsEnu.Current; - distance = hit.distance; - if (partHit == part) + // ignoring collisions against the explosive, or explosive vessel for certain explosive types (e.g., missile/rocket casing) + if (partHit == explosivePart || (explosivePart != null && ignoreCasingFor.Contains(ExplosionSource) && partHit.vessel == explosivePart.vessel)) { - return true; + continue; } - if (partHit != part) + if (FlightGlobals.currentMainBody != null && hit.collider.gameObject == FlightGlobals.currentMainBody.gameObject) return false; // Terrain hit. Full absorption. Should avoid NREs in the following. FIXME This doesn't seem correct anymore: "Kerbin Zn1232223233" vs "Kerbin", but doesn't seem to cause issues either. + if (intermediateParts) { - // ignoring collisions against the explosive, or explosive vessel for certain explosive types (e.g., missile/rocket casing) - if (partHit == explosivePart || (explosivePart != null && ignoreCasingFor.Contains(ExplosionSource) && partHit.vessel == explosivePart.vessel)) + var partHP = partHit.Damage(); + if (ProjectileUtils.IsArmorPart(partHit)) partHP = BDArmorySettings.EXP_PEN_RESIST_MULT * 100; + //var partArmour = partHit.GetArmorThickness(); + float partArmour = 0f; + var Armor = partHit.FindModuleImplementing(); + if (Armor != null && partHit.Rigidbody != null) { - continue; + float armorCos = Mathf.Abs(Vector3.Dot((hit.point + partHit.Rigidbody.velocity * TimeIndex - Position).normalized, -hit.normal)); + partArmour = ProjectileUtils.CalculateThickness(part, armorCos); + + if (warheadType == WarheadTypes.ShapedCharge) + { + partArmour *= Armor.HEATEquiv; + } + else + { + partArmour *= Armor.HEEquiv; + } + + //if (BDArmorySettings.DEBUG_WEAPONS) + //{ + // Debug.Log($"[BDArmory.ExplosionFX] Part: {partHit.name}; Thickness: {partArmour}mm; Angle: {Mathf.Rad2Deg * Mathf.Acos(armorCos)}; Contributed: {factor * Mathf.Max(partArmour / armorCos, 1)}mm; Distance: {hit.distance};"); + //} + + partArmour *= factor; + + factor *= 1.05f; } - if (FlightGlobals.currentMainBody != null && hit.collider.gameObject == FlightGlobals.currentMainBody.gameObject) return false; // Terrain hit. Full absorption. Should avoid NREs in the following. - var partHP = partHit.Damage(); - if (part.name.ToLower().Contains("armor")) partHP = 100; - var partArmour = partHit.GetArmorThickness(); var RA = partHit.FindModuleImplementing(); if (RA != null) { @@ -484,7 +609,7 @@ private bool IsInLineOfSight(Part part, Part explosivePart, out RaycastHit hit, } else { - if (((ExplosionSource == ExplosionSourceType.Bullet || ExplosionSource == ExplosionSourceType.Rocket) && (Caliber > RA.sensitivity && distance < 0.1f)) || //bullet/rocket hit + if (((ExplosionSource == ExplosionSourceType.Bullet || ExplosionSource == ExplosionSourceType.Rocket) && (Caliber > RA.sensitivity && partHit == projectileHitPart)) || //bullet/rocket hit ((ExplosionSource == ExplosionSourceType.Missile || ExplosionSource == ExplosionSourceType.BattleDamage) && (distance < Power / 2))) //or close range detonation likely to trigger ERA { partArmour = 300 * RA.armorModifier; @@ -492,16 +617,27 @@ private bool IsInLineOfSight(Part part, Part explosivePart, out RaycastHit hit, } } if (partHP > 0) // Ignore parts that are already dead but not yet removed from the game. - intermediateParts.Add(new Tuple(hit.distance, partHP, partArmour)); + LoSIntermediateParts.Add(new ValueTuple(hit.distance, partHP, partArmour)); } } + } - hit = new RaycastHit(); - distance = 0; + hit = miss; + distance = float.PositiveInfinity; return false; } - public void Update() + int CollateHits(ref RaycastHit[] forwardHits, int forwardHitCount, ref RaycastHit[] reverseHits, int reverseHitCount) + { + var totalHitCount = forwardHitCount + reverseHitCount; + if (sortedLoSHits.Length < totalHitCount) Array.Resize(ref sortedLoSHits, totalHitCount); + Array.Copy(forwardHits, sortedLoSHits, forwardHitCount); + Array.Copy(reverseHits, 0, sortedLoSHits, forwardHitCount, reverseHitCount); + Array.Sort(sortedLoSHits, 0, totalHitCount, RaycastHitComparer.raycastHitComparer); // This generates garbage, but less than other methods using Linq or Lists. + return totalHitCount; + } + + void Update() { if (!gameObject.activeInHierarchy) return; @@ -522,10 +658,20 @@ public void FixedUpdate() { if (!gameObject.activeInHierarchy) return; + if (UI.BDArmorySetup.GameIsPaused) + { + if (audioSource.isPlaying) + { + audioSource.Stop(); + } + return; + } + //floating origin and velocity offloading corrections - if (!FloatingOrigin.Offset.IsZero() || !Krakensbane.GetFrameVelocity().IsZero()) + if (BDKrakensbane.IsActive) { - transform.position -= FloatingOrigin.OffsetNonKrakensbane; + transform.position -= BDKrakensbane.FloatingOriginOffsetNonKrakensbane; + Position -= BDKrakensbane.FloatingOriginOffsetNonKrakensbane; } if (!isFX) { @@ -547,7 +693,7 @@ public void FixedUpdate() if (disabled && explosionEvents.Count == 0 && TimeIndex > MaxTime) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_OTHER) { Debug.Log("[BDArmory.ExplosionFX]: Explosion Finished"); } @@ -556,33 +702,34 @@ public void FixedUpdate() return; } } - + /* + ///////// + // Debugging for Continuous rod/shaped charge orientation, unnecessary unless something gets changed at somepoint, so commented out for now. + /////////// void OnGUI() { - if (HighLogic.LoadedSceneIsFlight && BDArmorySettings.DRAW_DEBUG_LINES) + if (HighLogic.LoadedSceneIsFlight && BDArmorySettings.DEBUG_LINES) { if (warheadType == WarheadTypes.ContinuousRod) { if (explosionEventsPartsAdded.Count > 0) { + RaycastHit hit; + float distance; for (int i = 0; i < explosionEventsPartsAdded.Count; i++) { - RaycastHit hit; - float distance; - List> intermediateParts; - try { Part part = explosionEventsPartsAdded[i]; - if (IsInLineOfSight(part, null, out hit, out distance, out intermediateParts)) + if (IsInLineOfSight(part, null, -1, out hit, out distance, false)) { if (IsAngleAllowed(Direction, hit, explosionEventsPartsAdded[i])) { - BDGUIUtils.DrawLineBetweenWorldPositions(Position, hit.point, 2, Color.blue); + GUIUtils.DrawLineBetweenWorldPositions(Position, hit.point, 2, Color.blue); } - if (distance < Range / 2) + else if (distance < Range / 2) { - BDGUIUtils.DrawLineBetweenWorldPositions(Position, hit.point, 2, Color.red); + GUIUtils.DrawLineBetweenWorldPositions(Position, hit.point, 2, Color.red); } } } @@ -595,261 +742,348 @@ void OnGUI() } if (warheadType == WarheadTypes.ShapedCharge) { - BDGUIUtils.DrawLineBetweenWorldPositions(Position, (Position + (Direction.normalized * Range)), 4, Color.green); + GUIUtils.DrawLineBetweenWorldPositions(Position, (Position + (Direction.normalized * Range)), 4, Color.green); } } } - + */ private void ExecuteBuildingBlastEvent(BuildingBlastHitEvent eventToExecute) { + if (BDArmorySettings.BUILDING_DMG_MULTIPLIER == 0) return; //TODO: Review if the damage is sensible after so many changes //buildings DestructibleBuilding building = eventToExecute.Building; - building.damageDecay = 600f; + //building.damageDecay = 600f; - if (building) + if (building && building.IsIntact && !BDArmorySettings.PAINTBALL_MODE) { var distanceFactor = Mathf.Clamp01((Range - eventToExecute.Distance) / Range); - float damageToBuilding = (BDArmorySettings.DMG_MULTIPLIER / 100) * BDArmorySettings.EXP_DMG_MOD_BALLISTIC_NEW * Power * distanceFactor; - - damageToBuilding *= 2f; - - building.AddDamage(damageToBuilding); - - if (building.Damage > building.impactMomentumThreshold) + float blastMod = 1; + switch (ExplosionSource) { + case ExplosionSourceType.Bullet: + blastMod = BDArmorySettings.EXP_DMG_MOD_BALLISTIC_NEW; + break; + case ExplosionSourceType.Rocket: + blastMod = BDArmorySettings.EXP_DMG_MOD_ROCKET; + break; + case ExplosionSourceType.Missile: + blastMod = BDArmorySettings.EXP_DMG_MOD_MISSILE; + break; + case ExplosionSourceType.BattleDamage: + blastMod = BDArmorySettings.EXP_DMG_MOD_BATTLE_DAMAGE; + break; + } + float damageToBuilding = (BDArmorySettings.DMG_MULTIPLIER / 100) * blastMod * (Power * distanceFactor); + damageToBuilding /= 2; + damageToBuilding *= BDArmorySettings.BUILDING_DMG_MULTIPLIER; + //building.AddDamage(damageToBuilding); + BuildingDamage.RegisterDamage(building); + building.FacilityDamageFraction += damageToBuilding; + //based on testing, I think facilityDamageFraction starts at values between 5 and 100, and demolished the building if it hits 0 - which means it will work great as a HP value in the other direction + if (building.FacilityDamageFraction > building.impactMomentumThreshold * 2) + { + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log($"[BDArmory.ExplosionFX]: Building {building.name} demolished due to Explosive damage! Dmg to building: {building.Damage}"); building.Demolish(); } - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_DAMAGE) { - Debug.Log("[BDArmory.ExplosionFX]: Explosion hit destructible building! Hitpoints Applied: " + Mathf.Round(damageToBuilding) + - ", Building Damage : " + Mathf.Round(building.Damage) + - " Building Threshold : " + building.impactMomentumThreshold); + Debug.Log($"[BDArmory.ExplosionFX]: Explosion hit destructible building {building.name}! Hitpoints Applied: {damageToBuilding:F3}, Building Damage: {building.FacilityDamageFraction}, Building Threshold : {building.impactMomentumThreshold * 2}, (Range: {Range}, Distance: {eventToExecute.Distance}, Factor: {distanceFactor}, Power: {Power})"); } } } private void ExecutePartBlastEvent(PartBlastHitEvent eventToExecute) { - if (eventToExecute.Part == null || eventToExecute.Part.Rigidbody == null || eventToExecute.Part.vessel == null || eventToExecute.Part.partInfo == null) return; + if (eventToExecute.Part == null || eventToExecute.Part.Rigidbody == null || eventToExecute.Part.vessel == null || eventToExecute.Part.partInfo == null) { eventToExecute.Finished(); return; } Part part = eventToExecute.Part; Rigidbody rb = part.Rigidbody; var realDistance = eventToExecute.Distance; var vesselMass = part.vessel.totalMass; if (vesselMass == 0) vesselMass = part.mass; // Sometimes if the root part is the only part of the vessel, then part.vessel.totalMass is 0, despite the part.mass not being 0. + bool shapedEffect = ((warheadType == WarheadTypes.ShapedCharge || warheadType == WarheadTypes.ContinuousRod) && eventToExecute.withinAngleofEffect); - if (!eventToExecute.IsNegativePressure) + + if (BDArmorySettings.DEBUG_WEAPONS && shapedEffect) { - BlastInfo blastInfo; + Debug.Log($"[BDArmory.ExplosionFX] Part: {part.name}; Real Distance: {realDistance}m; SCRange: {SCRange}m;"); + } - if (eventToExecute.withinAngleofEffect) //within AoE of shaped warheads, or otherwise standard blast - { - blastInfo = BlastPhysicsUtils.CalculatePartBlastEffects(part, realDistance, vesselMass * 1000f, Power, Range); - } - else //majority of force concentrated in blast cone for shaped warheads, not going to apply much force to stuff outside - { - if (realDistance < Range / 2) - { - blastInfo = BlastPhysicsUtils.CalculatePartBlastEffects(part, realDistance, vesselMass * 1000f, Power / 4, Range / 2); - } - else return; - } - //Debug.Log("[ExplosionFX] " + part.name + " Within AoE of detonation: " + eventToExecute.withinAngleofEffect); - // Overly simplistic approach: simply reduce damage by amount of HP/2 and Armor in the way. (HP/2 to simulate weak parts not fully blocking damage.) Does not account for armour reduction or angle of incidence of intermediate parts. - // A better approach would be to properly calculate the damage and pressure in CalculatePartBlastEffects due to the series of parts in the way. - var damageWithoutIntermediateParts = blastInfo.Damage; - var cumulativeHPOfIntermediateParts = eventToExecute.IntermediateParts.Select(p => p.Item2).Sum(); - var cumulativeArmorOfIntermediateParts = eventToExecute.IntermediateParts.Select(p => p.Item3).Sum(); - blastInfo.Damage = Mathf.Max(0f, blastInfo.Damage - 0.5f * cumulativeHPOfIntermediateParts - cumulativeArmorOfIntermediateParts); - - if (CASEClamp > 0) + if ((realDistance <= Range) || (realDistance <= SCRange)) //within radius of Blast + { + if (!eventToExecute.IsNegativePressure) { - if (CASEClamp < 1000) + BlastInfo blastInfo; + + if (eventToExecute.withinAngleofEffect) //within AoE of shaped warheads, or otherwise standard blast { - blastInfo.Damage = Mathf.Clamp(blastInfo.Damage, 0, Mathf.Min((part.Modules.GetModule().GetMaxHitpoints() * 0.9f), CASEClamp)); + blastInfo = BlastPhysicsUtils.CalculatePartBlastEffects(part, realDistance, vesselMass * 1000f, Power, Range); } - else + else //majority of force concentrated in blast AoE for shaped warheads, not going to apply much force to stuff outside { - blastInfo.Damage = Mathf.Clamp(blastInfo.Damage, 0, CASEClamp); + if (realDistance < Range / 2) //further away than half the blast range, falloff blast effect outside primary AoE + { + blastInfo = BlastPhysicsUtils.CalculatePartBlastEffects(part, realDistance, vesselMass * 1000f, Power / 3, Range / 2); + } + else { eventToExecute.Finished(); return; } } - } + //if (BDArmorySettings.DEBUG_LABELS) Debug.Log("[BDArmory.ExplosionFX]: " + part.name + " Within AoE of detonation: " + eventToExecute.withinAngleofEffect); + // Overly simplistic approach: simply reduce damage by amount of HP/2 and Armor in the way. (HP/2 to simulate weak parts not fully blocking damage.) Does not account for armour reduction or angle of incidence of intermediate parts. + // A better approach would be to properly calculate the damage and pressure in CalculatePartBlastEffects due to the series of parts in the way. - if (blastInfo.Damage > 0) - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - Debug.Log( - "[BDArmory.ExplosionFX]: Executing blast event Part: {" + part.name + "}, " + - " VelocityChange: {" + blastInfo.VelocityChange + "}," + - " Distance: {" + realDistance + "}," + - " TotalPressure: {" + blastInfo.TotalPressure + "}," + - " Damage: {" + blastInfo.Damage + "} (reduced from " + damageWithoutIntermediateParts + " by " + eventToExecute.IntermediateParts.Count + " parts)," + - " EffectiveArea: {" + blastInfo.EffectivePartArea + "}," + - " Positive Phase duration: {" + blastInfo.PositivePhaseDuration + "}," + - " Vessel mass: {" + Math.Round(vesselMass * 1000f) + "}," + - " TimeIndex: {" + TimeIndex + "}," + - " TimePlanned: {" + eventToExecute.TimeToImpact + "}," + - " NegativePressure: {" + eventToExecute.IsNegativePressure + "}"); - } + var cumulativeHPOfIntermediateParts = eventToExecute.IntermediateParts.Select(p => p.Item2).Sum(); + var cumulativeArmorOfIntermediateParts = eventToExecute.IntermediateParts.Select(p => p.Item3).Sum(); + var damageWithoutIntermediateParts = blastInfo.Damage; - // Add Reverse Negative Event - explosionEvents.Enqueue(new PartBlastHitEvent() - { - Distance = Range - realDistance, - Part = part, - TimeToImpact = 2 * (Range / ExplosionVelocity) + (Range - realDistance) / ExplosionVelocity, - IsNegativePressure = true, - NegativeForce = blastInfo.VelocityChange * 0.25f - }); + blastInfo.Damage = Mathf.Max(0f, blastInfo.Damage - 0.5f * cumulativeHPOfIntermediateParts - cumulativeArmorOfIntermediateParts); - if (rb != null && rb.mass > 0 && !BDArmorySettings.PAINTBALL_MODE) - { - AddForceAtPosition(rb, - (eventToExecute.HitPoint + rb.velocity * TimeIndex - Position).normalized * - blastInfo.VelocityChange * - BDArmorySettings.EXP_IMP_MOD, - eventToExecute.HitPoint + rb.velocity * TimeIndex); - } - var damage = 0f; - float penetrationFactor = 0.5f; - if (dmgMult < 0) + if (CASEClamp > 0) { - part.AddInstagibDamage(); - //Debug.Log("[ExplosionFX] applying instagib!"); + if (CASEClamp < 1000) + { + blastInfo.Damage = Mathf.Clamp(blastInfo.Damage, 0, Mathf.Min((part.Modules.GetModule().GetMaxHitpoints() * 0.9f), CASEClamp)); + } + else + { + blastInfo.Damage = Mathf.Clamp(blastInfo.Damage, 0, CASEClamp); + } } - var RA = part.FindModuleImplementing(); - if (RA != null && !RA.NXRA && (ExplosionSource == ExplosionSourceType.Bullet || ExplosionSource == ExplosionSourceType.Rocket) && (Caliber > RA.sensitivity && realDistance < 0.1f)) //bullet/rocket hit - { - RA.UpdateSectionScales(); - } - else + if (blastInfo.Damage > 0 || shapedEffect) { - if ((warheadType == WarheadTypes.ShapedCharge || warheadType == WarheadTypes.ContinuousRod) && eventToExecute.withinAngleofEffect) + if (BDArmorySettings.DEBUG_DAMAGE) { - float HitAngle = Vector3.Angle((eventToExecute.HitPoint + rb.velocity * TimeIndex - Position).normalized, -eventToExecute.Hit.normal); - float anglemultiplier = (float)Math.Cos(Math.PI * HitAngle / 180.0); - float thickness = ProjectileUtils.CalculateThickness(part, anglemultiplier); - if (BDArmorySettings.DRAW_ARMOR_LABELS) Debug.Log("[BDArmory.ExplosiveFX]: Part " + part.name + " hit by " + warheadType + "; " + HitAngle + " deg hit, armor thickness: " + thickness); - thickness += eventToExecute.IntermediateParts.Select(p => p.Item3).Sum(); //add armor thickness of intervening parts, if any - if (BDArmorySettings.DRAW_ARMOR_LABELS) Debug.Log("[BDArmory.ExplosiveFX]: Effective Armor thickness from intermediate parts: " + thickness); - float penetration = 0; - var Armor = part.FindModuleImplementing(); - if (Armor != null) - { - float Ductility = Armor.Ductility; - float hardness = Armor.Hardness; - float Strength = Armor.Strength; - float safeTemp = Armor.SafeUseTemp; - float Density = Armor.Density; - int type = (int)Armor.ArmorTypeNum; + Debug.Log( + $"[BDArmory.ExplosionFX]: Executing blast event Part: [{part.name}], VelocityChange: [{blastInfo.VelocityChange}], Distance: [{realDistance}]," + + $" TotalPressure: [{blastInfo.TotalPressure}], Damage: [{blastInfo.Damage}] (reduced from {damageWithoutIntermediateParts} by {eventToExecute.IntermediateParts.Count} parts)," + + $" EffectiveArea: [{blastInfo.EffectivePartArea}], Positive Phase duration: [{blastInfo.PositivePhaseDuration}]," + + $" Vessel mass: [{Math.Round(vesselMass * 1000f)}], TimeIndex: [{TimeIndex}], TimePlanned: [{eventToExecute.TimeToImpact}], NegativePressure: [{eventToExecute.IsNegativePressure}]"); + } - penetration = ProjectileUtils.CalculatePenetration(Caliber, Caliber, warheadType == WarheadTypes.ShapedCharge ? Power / 2 : ProjMass, ExplosionVelocity, Ductility, Density, Strength, thickness, 1); - penetrationFactor = ProjectileUtils.CalculateArmorPenetration(part, penetration, thickness); + // Add Reverse Negative Event + explosionEvents.Enqueue(new PartBlastHitEvent() + { + Distance = Range - realDistance, + Part = part, + TimeToImpact = 2 * (Range / ExplosionVelocity) + (Range - realDistance) / ExplosionVelocity, + IsNegativePressure = true, + NegativeForce = blastInfo.VelocityChange * 0.25f + }); + + if (rb != null && rb.mass > 0 && !BDArmorySettings.PAINTBALL_MODE) + { + AddForceAtPosition(rb, + (eventToExecute.HitPoint + rb.velocity * TimeIndex - Position).normalized * + blastInfo.VelocityChange * + BDArmorySettings.EXP_IMP_MOD, + eventToExecute.HitPoint + rb.velocity * TimeIndex); + } + var damage = 0f; + float penetrationFactor = 0.5f; + if (dmgMult < 0) + { + part.AddInstagibDamage(); + //if (BDArmorySettings.DEBUG_LABELS) Debug.Log("[BDArmory.ExplosionFX]: applying instagib!"); + } + var RA = part.FindModuleImplementing(); - if (RA != null) - { - if (penetrationFactor > 1) - { - float thicknessModifier = RA.armorModifier; - if (BDArmorySettings.DRAW_ARMOR_LABELS) Debug.Log("[ExplosionFX] Beginning Reactive Armor Hit; NXRA: " + RA.NXRA + "; thickness Mod: " + RA.armorModifier); - if (RA.NXRA) //non-explosive RA, always active - { - thickness *= thicknessModifier; - } - else - { - RA.UpdateSectionScales(); - } - } - penetrationFactor = ProjectileUtils.CalculateArmorPenetration(part, penetration, thickness); //RA stop round? - } - else ProjectileUtils.CalculateArmorDamage(part, penetrationFactor, Caliber, hardness, Ductility, Density, ExplosionVelocity, SourceVesselName, ExplosionSourceType.Missile, type); - } - BulletHitFX.CreateBulletHit(part, eventToExecute.HitPoint, eventToExecute.Hit, eventToExecute.Hit.normal, true, Caliber, penetrationFactor, null); - if (penetrationFactor > 1) - { - damage = part.AddExplosiveDamage(blastInfo.Damage, Caliber, ExplosionSource, dmgMult); - if (float.IsNaN(damage)) Debug.LogError("DEBUG NaN damage!"); - } + if (RA != null && !RA.NXRA && (ExplosionSource == ExplosionSourceType.Bullet || ExplosionSource == ExplosionSourceType.Rocket) && (Caliber > RA.sensitivity && realDistance <= 0.1f)) //bullet/rocket hit + { + RA.UpdateSectionScales(); } else { - if ((part == hitpart && part.name.ToLower().Contains("armor")) || !ProjectileUtils.CalculateExplosiveArmorDamage(part, blastInfo.TotalPressure, SourceVesselName, eventToExecute.Hit, ExplosionSource)) //false = armor blowthrough or bullet detonating inside part + if (shapedEffect && ((warheadType == WarheadTypes.ShapedCharge) ? (realDistance <= SCRange) : warheadType == WarheadTypes.ContinuousRod)) { - if (RA != null && !RA.NXRA) //blast wave triggers RA; detonate all remaining RA sections + //float HitAngle = Vector3.Angle((eventToExecute.HitPoint + rb.velocity * TimeIndex - Position).normalized, -eventToExecute.Hit.normal); + //float anglemultiplier = (float)Math.Cos(Math.PI * HitAngle / 180.0); + float anglemultiplier = Mathf.Abs(Vector3.Dot((eventToExecute.HitPoint + rb.velocity * TimeIndex - Position).normalized, -eventToExecute.Hit.normal)); + float thickness = ProjectileUtils.CalculateThickness(part, anglemultiplier); + if (BDArmorySettings.DEBUG_ARMOR) Debug.Log($"[BDArmory.ExplosionFX]: Part {part.name} hit by {warheadType}; {Mathf.Rad2Deg * Mathf.Acos(anglemultiplier)} deg hit, armor thickness: {thickness}"); + //float thicknessBetween = eventToExecute.IntermediateParts.Select(p => p.Item3).Sum(); //add armor thickness of intervening parts, if any + if (BDArmorySettings.DEBUG_ARMOR) Debug.Log($"[BDArmory.ExplosionFX]: Effective Armor thickness from intermediate parts: {thickness}"); + //float penetration = 0; + float standoffTemp = realDistance / (14f * Caliber * 20f * 0.001f); + float standoffFactor = 1f / (1f + standoffTemp * standoffTemp); + + float remainingPen = penetration * standoffFactor - cumulativeArmorOfIntermediateParts; + + var Armor = part.FindModuleImplementing(); + if (Armor != null) { - for (int i = 0; i < RA.sectionsRemaining; i++) + float Ductility = Armor.Ductility; + float hardness = Armor.Hardness; + float Strength = Armor.Strength; + float safeTemp = Armor.SafeUseTemp; + float Density = Armor.Density; + float armorEquiv = warheadType == WarheadTypes.ShapedCharge ? Armor.HEATEquiv : Armor.HEEquiv; + //float vFactor = Armor.vFactor; + //float muParam1 = Armor.muParam1; + //float muParam2 = Armor.muParam2; + //float muParam3 = Armor.muParam3; + int type = (int)Armor.ArmorTypeNum; + + //penetration = ProjectileUtils.CalculatePenetration(Caliber, Caliber, warheadType == WarheadTypes.ShapedCharge ? Power / 2 : ProjMass, ExplosionVelocity, Ductility, Density, Strength, thickness, 1); + // Moved penetration since it's now calculated off of a universal material rather than specific materials + + penetrationFactor = ProjectileUtils.CalculateArmorPenetration(part, remainingPen, thickness * armorEquiv); + + if (BDArmorySettings.DEBUG_WEAPONS) { - RA.UpdateSectionScales(); + Debug.Log($"[BDArmory.ExplosionFX] Penetration: {penetration} mm; Thickness: {thickness * armorEquiv} mm; armorEquiv: {armorEquiv}; Intermediate Armor: {penetration * standoffFactor - remainingPen} mm; Remaining Penetration: {remainingPen} mm; Penetration Factor: {penetrationFactor}; Standoff Factor: {standoffFactor}"); + } + + if (RA != null) + { + if (penetrationFactor > 1) + { + float thicknessModifier = RA.armorModifier; + if (BDArmorySettings.DEBUG_ARMOR) Debug.Log($"[BDArmory.ExplosionFX]: Beginning Reactive Armor Hit; NXRA: {RA.NXRA}; thickness Mod: {RA.armorModifier}"); + if (RA.NXRA) //non-explosive RA, always active + { + thickness *= thicknessModifier; + } + else + { + RA.UpdateSectionScales(); + eventToExecute.Finished(); + return; + } + } + penetrationFactor = ProjectileUtils.CalculateArmorPenetration(part, remainingPen, thickness * armorEquiv); //RA stop round? + } + //else ProjectileUtils.CalculateArmorDamage(part, penetrationFactor, Caliber, hardness, Ductility, Density, ExplosionVelocity, SourceVesselName, ExplosionSourceType.Missile, type); + else if (penetrationFactor > 0) + { + ProjectileUtils.CalculateArmorDamage(part, penetrationFactor, Caliber * 2.5f, hardness, Ductility, Density, warheadType == WarheadTypes.ShapedCharge ? 5000f : ExplosionVelocity, SourceVesselName, ExplosionSourceType.Missile, type); } } else { - damage = part.AddExplosiveDamage(blastInfo.Damage, Caliber, ExplosionSource, dmgMult); - penetrationFactor = damage / 10; //closer to the explosion/greater magnitude of the explosion at point blank, the greater the blowthrough + // Based on 10 mm of aluminium + penetrationFactor = 10f * (warheadType == WarheadTypes.ShapedCharge ? 0.5528789891f : 0.1601427673f) / (remainingPen); + } + + if (penetrationFactor > 0) + { + BulletHitFX.CreateBulletHit(part, eventToExecute.HitPoint, eventToExecute.Hit, eventToExecute.Hit.normal, true, Caliber, penetrationFactor > 0 ? penetrationFactor : 0f, null); + damage = part.AddBallisticDamage(warheadType == WarheadTypes.ShapedCharge ? Power * 0.0555f : ProjMass, Caliber, 1f, penetrationFactor, dmgMult, warheadType == WarheadTypes.ShapedCharge ? 5000f : ExplosionVelocity, ExplosionSourceType.Missile); + } + + if (penetrationFactor > 1) + { + if (blastInfo.Damage > 0) + { + damage += part.AddExplosiveDamage(blastInfo.Damage, Caliber, ExplosionSource, dmgMult); + } + if (float.IsNaN(damage)) Debug.LogError("DEBUG NaN damage!"); } } - } - if (damage > 0) //else damage from spalling done in CalcExplArmorDamage - { - if (BDArmorySettings.BATTLEDAMAGE) + else { - BattleDamageHandler.CheckDamageFX(part, Caliber, penetrationFactor, true, warheadType == WarheadTypes.ShapedCharge ? true : false, SourceVesselName, eventToExecute.Hit); + if ((part == projectileHitPart && ProjectileUtils.IsArmorPart(part)) || !ProjectileUtils.CalculateExplosiveArmorDamage(part, blastInfo.TotalPressure, realDistance, SourceVesselName, eventToExecute.Hit, ExplosionSource, Range - realDistance)) //false = armor blowthrough or bullet detonating inside part + { + if (RA != null && !RA.NXRA) //blast wave triggers RA; detonate all remaining RA sections + { + for (int i = 0; i < RA.sectionsRemaining; i++) + { + RA.UpdateSectionScales(); + } + } + else + { + damage = part.AddExplosiveDamage(blastInfo.Damage, Caliber, ExplosionSource, dmgMult); + if (part == projectileHitPart && ProjectileUtils.IsArmorPart(part)) //deal armor damage to armor panel, since we didn't do that earlier + { + ProjectileUtils.CalculateExplosiveArmorDamage(part, blastInfo.TotalPressure, realDistance, SourceVesselName, eventToExecute.Hit, ExplosionSource, Range - realDistance); + } + penetrationFactor = damage / 10; //closer to the explosion/greater magnitude of the explosion at point blank, the greater the blowthrough + if (float.IsNaN(damage)) Debug.LogError("DEBUG NaN damage!"); + } + } } - // Update scoring structures - //damage = Mathf.Clamp(damage, 0, part.Damage()); //if we want to clamp overkill score inflation - var aName = eventToExecute.SourceVesselName; // Attacker - var tName = part.vessel.GetName(); // Target - switch (ExplosionSource) + if (damage > 0) //else damage from spalling done in CalcExplArmorDamage { - case ExplosionSourceType.Bullet: - BDACompetitionMode.Instance.Scores.RegisterBulletDamage(aName, tName, damage); - break; - case ExplosionSourceType.Rocket: - BDACompetitionMode.Instance.Scores.RegisterRocketDamage(aName, tName, damage); - break; - case ExplosionSourceType.Missile: - BDACompetitionMode.Instance.Scores.RegisterMissileDamage(aName, tName, damage); - break; - case ExplosionSourceType.BattleDamage: - BDACompetitionMode.Instance.Scores.RegisterBattleDamage(aName, part.vessel, damage); - break; + if (BDArmorySettings.BATTLEDAMAGE) + { + BattleDamageHandler.CheckDamageFX(part, Caliber, penetrationFactor, true, warheadType == WarheadTypes.ShapedCharge ? true : false, SourceVesselName, eventToExecute.Hit); + } + // Update scoring structures + //damage = Mathf.Clamp(damage, 0, part.Damage()); //if we want to clamp overkill score inflation + var aName = eventToExecute.SourceVesselName; // Attacker + var tName = part.vessel.GetName(); // Target + switch (ExplosionSource) + { + case ExplosionSourceType.Bullet: + BDACompetitionMode.Instance.Scores.RegisterBulletDamage(aName, tName, damage); + break; + case ExplosionSourceType.Rocket: + BDACompetitionMode.Instance.Scores.RegisterRocketDamage(aName, tName, damage); + break; + case ExplosionSourceType.Missile: + BDACompetitionMode.Instance.Scores.RegisterMissileDamage(aName, tName, damage); + break; + case ExplosionSourceType.BattleDamage: + BDACompetitionMode.Instance.Scores.RegisterBattleDamage(aName, part.vessel, damage); + break; + } } } } + else if (BDArmorySettings.DEBUG_DAMAGE) + { + Debug.Log($"[BDArmory.ExplosionFX]: Part {part.name} at distance {realDistance}m took no damage due to parts with {cumulativeHPOfIntermediateParts} HP and {cumulativeArmorOfIntermediateParts} Armor in the way."); + } } - else if (BDArmorySettings.DRAW_DEBUG_LABELS) + else { - Debug.Log("[BDArmory.ExplosiveFX]: Part " + part.name + " at distance " + realDistance + "m took no damage due to parts with " + cumulativeHPOfIntermediateParts + "HP and " + cumulativeArmorOfIntermediateParts + " Armor in the way."); + if (BDArmorySettings.DEBUG_DAMAGE) + { + Debug.Log( + $"[BDArmory.ExplosionFX]: Executing blast event Part: [{part.name}], VelocityChange: [{eventToExecute.NegativeForce}], Distance: [{realDistance}]," + + $" Vessel mass: [{Math.Round(vesselMass * 1000f)}], TimeIndex: [{TimeIndex}], TimePlanned: [{eventToExecute.TimeToImpact}], NegativePressure: [{eventToExecute.IsNegativePressure}]"); + } + if (rb != null && rb.mass > 0 && !BDArmorySettings.PAINTBALL_MODE) + AddForceAtPosition(rb, (Position - part.transform.position).normalized * eventToExecute.NegativeForce * BDArmorySettings.EXP_IMP_MOD * 0.25f, part.transform.position); } + eventToExecute.Finished(); } - else + if (warheadType == WarheadTypes.Standard && ProjMass > 0 && realDistance <= blastRange) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + //float HitAngle = Vector3.Angle((eventToExecute.HitPoint + rb.velocity * TimeIndex - Position).normalized, -eventToExecute.Hit.normal); + //float anglemultiplier = (float)Math.Cos(Math.PI * HitAngle / 180.0); + float anglemultiplier = Mathf.Abs(Vector3.Dot((eventToExecute.HitPoint + rb.velocity * TimeIndex - Position).normalized, -eventToExecute.Hit.normal)); + float thickness = ProjectileUtils.CalculateThickness(part, anglemultiplier); + var Armor = part.FindModuleImplementing(); + if (Armor != null) { - Debug.Log( - "[BDArmory.ExplosionFX]: Executing blast event Part: {" + part.name + "}, " + - " VelocityChange: {" + eventToExecute.NegativeForce + "}," + - " Distance: {" + realDistance + "}," + - " Vessel mass: {" + Math.Round(vesselMass * 1000f) + "}," + - " TimeIndex: {" + TimeIndex + "}," + - " TimePlanned: {" + eventToExecute.TimeToImpact + "}," + - " NegativePressure: {" + eventToExecute.IsNegativePressure + "}"); + thickness *= Armor.HEEquiv; } - if (rb != null && rb.mass > 0 && !BDArmorySettings.PAINTBALL_MODE) - AddForceAtPosition(rb, (Position - part.transform.position).normalized * eventToExecute.NegativeForce * BDArmorySettings.EXP_IMP_MOD * 0.25f, part.transform.position); + thickness += eventToExecute.IntermediateParts.Select(p => p.Item3).Sum(); //add armor thickness of intervening parts, if any + if (BDArmorySettings.DEBUG_ARMOR) Debug.Log($"[BDArmory.ExplosiveFX]: Part {part.name} hit by shrapnel; {Mathf.Rad2Deg * Mathf.Acos(anglemultiplier)} deg hit, cumulative armor thickness: {thickness}"); + + ProjectileUtils.CalculateShrapnelDamage(part, eventToExecute.Hit, Caliber, Power, realDistance, SourceVesselName, ExplosionSource, ProjMass, -1, thickness); //part hit by shrapnel, but not pressure wave } } // We use an ObjectPool for the ExplosionFx instances as they leak KSPParticleEmitters otherwise. static void CreateObjectPool(string explModelPath, string soundPath) { - var key = explModelPath + soundPath; - if (!explosionFXPools.ContainsKey(key) || explosionFXPools[key] == null) + if (!string.IsNullOrEmpty(soundPath) && (!audioClips.ContainsKey(soundPath) || audioClips[soundPath] is null)) + { + var audioClip = SoundUtils.GetAudioClip(soundPath); + if (audioClip is null) + { + Debug.LogError("[BDArmory.ExplosionFX]: " + soundPath + " was not found, using the default sound instead. Please fix your model."); + audioClip = SoundUtils.GetAudioClip(ModuleWeapon.defaultExplSoundPath); + } + audioClips.Add(soundPath, audioClip); + } + + if (!explosionFXPools.ContainsKey(explModelPath) || explosionFXPools[explModelPath] == null) { var explosionFXTemplate = GameDatabase.Instance.GetModel(explModelPath); if (explosionFXTemplate == null) @@ -857,30 +1091,27 @@ static void CreateObjectPool(string explModelPath, string soundPath) Debug.LogError("[BDArmory.ExplosionFX]: " + explModelPath + " was not found, using the default explosion instead. Please fix your model."); explosionFXTemplate = GameDatabase.Instance.GetModel(ModuleWeapon.defaultExplModelPath); } - var soundClip = GameDatabase.Instance.GetAudioClip(soundPath); - if (soundClip == null) - { - Debug.LogError("[BDArmory.ExplosionFX]: " + soundPath + " was not found, using the default sound instead. Please fix your model."); - soundClip = GameDatabase.Instance.GetAudioClip(ModuleWeapon.defaultExplSoundPath); - } var eFx = explosionFXTemplate.AddComponent(); - eFx.ExSound = soundClip; eFx.audioSource = explosionFXTemplate.AddComponent(); eFx.audioSource.minDistance = 200; eFx.audioSource.maxDistance = 5500; eFx.audioSource.spatialBlend = 1; eFx.LightFx = explosionFXTemplate.AddComponent(); - eFx.LightFx.color = Utils.ParseColor255("255,238,184,255"); + eFx.LightFx.color = GUIUtils.ParseColor255("255,238,184,255"); eFx.LightFx.intensity = 8; eFx.LightFx.shadows = LightShadows.None; explosionFXTemplate.SetActive(false); - explosionFXPools[key] = ObjectPool.CreateObjectPool(explosionFXTemplate, 10, true, true, 0f, false); + explosionFXPools[explModelPath] = ObjectPool.CreateObjectPool(explosionFXTemplate, 10, true, true, 0f, false); } } public static void CreateExplosion(Vector3 position, float tntMassEquivalent, string explModelPath, string soundPath, ExplosionSourceType explosionSourceType, - float caliber = 120, Part explosivePart = null, string sourceVesselName = null, string sourceWeaponName = null, Vector3 direction = default(Vector3), float angle = 100f, bool isfx = false, float projectilemass = 0, float caseLimiter = -1, float dmgMutator = 1, string type = "standard", Part Hitpart = null) + float caliber = 120, Part explosivePart = null, string sourceVesselName = null, string sourceWeaponName = null, Vector3 direction = default(Vector3), + float angle = 100f, bool isfx = false, float projectilemass = 0, float caseLimiter = -1, float dmgMutator = 1, string type = "standard", Part Hitpart = null, + float apMod = 1f, float distancetravelled = -1) { + if (BDArmorySettings.DEBUG_MISSILES && explosionSourceType == ExplosionSourceType.Missile && (!explosionFXPools.ContainsKey(explModelPath) || !audioClips.ContainsKey(soundPath))) + { Debug.Log($"[BDArmory.ExplosionFX]: Setting up object pool for explosion of type {explModelPath} with audio {soundPath}{(sourceWeaponName != null ? $" for {sourceWeaponName}" : "")}"); } CreateObjectPool(explModelPath, soundPath); Quaternion rotation; @@ -893,7 +1124,7 @@ public static void CreateExplosion(Vector3 position, float tntMassEquivalent, st rotation = Quaternion.LookRotation(direction); } - GameObject newExplosion = explosionFXPools[explModelPath + soundPath].GetPooledObject(); + GameObject newExplosion = explosionFXPools[explModelPath].GetPooledObject(); newExplosion.transform.SetPositionAndRotation(position, rotation); ExplosionFx eFx = newExplosion.GetComponent(); eFx.Range = BlastPhysicsUtils.CalculateBlastRange(tntMassEquivalent); @@ -909,9 +1140,10 @@ public static void CreateExplosion(Vector3 position, float tntMassEquivalent, st eFx.ProjMass = projectilemass; eFx.CASEClamp = caseLimiter; eFx.dmgMult = dmgMutator; - eFx.hitpart = Hitpart; + eFx.projectileHitPart = Hitpart; eFx.pEmitters = newExplosion.GetComponentsInChildren(); eFx.audioSource = newExplosion.GetComponent(); + eFx.SoundPath = soundPath; type = type.ToLower(); switch (type) { @@ -923,18 +1155,48 @@ public static void CreateExplosion(Vector3 position, float tntMassEquivalent, st break; case "shapedcharge": eFx.warheadType = WarheadTypes.ShapedCharge; - eFx.AngleOfEffect = 10f; - eFx.Caliber = caliber > 0 ? caliber / 2 : 50; + //eFx.AngleOfEffect = 10f; + //eFx.AngleOfEffect = 5f; + eFx.cosAngleOfEffect = Mathf.Cos(Mathf.Deg2Rad * 5f); // cos(5 degrees) + eFx.Caliber = caliber > 0 ? caliber * 0.05f : 6f; + + // Hypervelocity jet caliber determined by rule of thumb equation for the caliber based on + // "The Hollow Charge Effect" Bulletin of the Institution of Mining and Metallurgy. No. 520, March 1950 + // by W. M. Evans. Jet is approximately 20% of the caliber. + + eFx.apMod = apMod; + eFx.travelDistance = distancetravelled; break; default: eFx.warheadType = WarheadTypes.Standard; - eFx.AngleOfEffect = angle >= 0f ? Mathf.Clamp(angle, 0f, 180f) : 100f; + eFx.cosAngleOfEffect = angle >= 0f ? Mathf.Clamp(angle, 0f, 180f) : 100f; + eFx.cosAngleOfEffect = Mathf.Cos(Mathf.Deg2Rad * eFx.cosAngleOfEffect); break; } + + if (type == "shapedcharge" || type == "continuousrod") + { + eFx.penetration = ProjectileUtils.CalculatePenetration(eFx.Caliber, type == "shapedcharge" ? 5000f : ExplosionVelocity, type == "shapedcharge" ? tntMassEquivalent * 0.0555f : eFx.ProjMass, apMod); + // Approximate fitting of mass to tntMass for modern shaped charges was done, + // giving the estimate of 0.0555*tntMass which works surprisingly well for modern + // warheads. 5000 m/s is around the average velocity of the jet. In reality, the + // jet has a velocity which linearly decreases from the tip to the tail, with the + // velocity being O(detVelocity) at the tip and O(1/4*detVelocity) at the tail. + // The linear estimate is also from "The Hollow Charge Effect", however this is + // too complex for the non-numerical penetration model used. Note that the density + // of the liner is far overestimated here, however this is accounted for in the + // estimate of the liner mass and the simple fit for liner mass of modern warheads + // is surprisingly good using the above formula. + } + else + { + eFx.penetration = 0; + } + if (direction == default(Vector3) && explosionSourceType == ExplosionSourceType.Missile) { eFx.warheadType = WarheadTypes.Standard; - Debug.Log("[BDArmory.ExplosionFX]: No direction param specified, defaulting warhead type!"); + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log("[BDArmory.ExplosionFX]: No direction param specified, defaulting warhead type!"); } if (tntMassEquivalent <= 5) { @@ -952,9 +1214,9 @@ public static void AddForceAtPosition(Rigidbody rb, Vector3 force, Vector3 posit ////////////////////////////////////////////////////////// if (rb == null || rb.mass == 0) return; rb.AddForceAtPosition(force, position, ForceMode.VelocityChange); - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_DAMAGE) { - Debug.Log("[BDArmory.ExplosionFX]: Force Applied | Explosive : " + Math.Round(force.magnitude, 2)); + Debug.Log($"[BDArmory.ExplosionFX]: Force Applied | Explosive : {Math.Round(force.magnitude, 2)}"); } } } @@ -973,12 +1235,56 @@ internal class PartBlastHitEvent : BlastHitEvent public RaycastHit Hit { get; set; } public float NegativeForce { get; set; } public string SourceVesselName { get; set; } - public List> IntermediateParts { get; set; } // distance, HP, armour public bool withinAngleofEffect { get; set; } + public List<(float, float, float)> IntermediateParts + { + get + { + if (_intermediateParts is not null && _intermediateParts.inUse) + return _intermediateParts.value; + else // It's a blank or null pool entry, set things up. + { + _intermediateParts = intermediatePartsPool.GetPooledObject(); + if (_intermediateParts.value is null) _intermediateParts.value = new List<(float, float, float)>(); + _intermediateParts.value.Clear(); + return _intermediateParts.value; + } + } + set // Note: this doesn't set the _intermediateParts.value to value, but rather copies the elements into the existing list. This should avoid excessive GC allocations. + { + if (_intermediateParts is null || !_intermediateParts.inUse) _intermediateParts = intermediatePartsPool.GetPooledObject(); + _intermediateParts.value.Clear(); + _intermediateParts.value.AddRange(value); + } + } // distance, HP, armour + + ObjectPoolEntry> _intermediateParts; + + public void Finished() // Return the IntermediateParts list back to the pool and free up memory. + { + if (_intermediateParts is null) return; + _intermediateParts.inUse = false; + if (_intermediateParts.value is null) return; + _intermediateParts.value.Clear(); + } + static ObjectPoolNonUnity> intermediatePartsPool = new ObjectPoolNonUnity>(); // Pool the IntermediateParts lists to avoid GC alloc. } + internal class BuildingBlastHitEvent : BlastHitEvent { public DestructibleBuilding Building { get; set; } } + + /// + /// Comparer for raycast hit sorting. + /// + internal class RaycastHitComparer : IComparer + { + int IComparer.Compare(RaycastHit left, RaycastHit right) + { + return left.distance.CompareTo(right.distance); + } + public static RaycastHitComparer raycastHitComparer = new RaycastHitComparer(); + } } diff --git a/BDArmory/FX/FXEmitter.cs b/BDArmory/FX/FXEmitter.cs index 10a97e849..0c432a98d 100644 --- a/BDArmory/FX/FXEmitter.cs +++ b/BDArmory/FX/FXEmitter.cs @@ -1,8 +1,11 @@ -using BDArmory.Misc; -using System; +using System; using System.Collections.Generic; using UnityEngine; +using BDArmory.Utils; +using BDArmory.Weapons; +using System.Collections; + namespace BDArmory.FX { class FXEmitter : MonoBehaviour @@ -12,6 +15,7 @@ class FXEmitter : MonoBehaviour public float StartTime { get; set; } public AudioClip ExSound { get; set; } public AudioSource audioSource { get; set; } + public string SoundPath { get; set; } private float Power { get; set; } private float emitTime { get; set; } private float maxTime { get; set; } @@ -50,6 +54,22 @@ private void OnEnable() emission.enabled = true; EffectBehaviour.AddParticleEmitter(pe); } + if (!String.IsNullOrEmpty(SoundPath)) + { + audioSource = gameObject.GetComponent(); + if (ExSound == null) + { + ExSound = SoundUtils.GetAudioClip(SoundPath); + + if (ExSound == null) + { + Debug.LogError("[BDArmory.FXEmitter]: " + ExSound + " was not found, using the default sound instead. Please fix your model."); + ExSound = SoundUtils.GetAudioClip(ModuleWeapon.defaultExplSoundPath); + } + } + audioSource.PlayOneShot(ExSound); //get distance to active vessel and add a delay? + //StartCoroutine(DelayBlastSFX(Vector3.Distance(this.transform.position, FlightGlobals.ActiveVessel.CoM) / 343f)); + } } void OnDisable() @@ -94,9 +114,18 @@ public void FixedUpdate() { if (!gameObject.activeInHierarchy) return; - if (!FloatingOrigin.Offset.IsZero() || !Krakensbane.GetFrameVelocity().IsZero()) + if (UI.BDArmorySetup.GameIsPaused) + { + if (audioSource.isPlaying) + { + audioSource.Stop(); + } + return; + } + + if (BDKrakensbane.IsActive) { - transform.position -= FloatingOrigin.OffsetNonKrakensbane; + transform.position -= BDKrakensbane.FloatingOriginOffsetNonKrakensbane; } if ((disabled || overrideLifeTime) && TimeIndex > particlesMaxEnergy) @@ -104,8 +133,23 @@ public void FixedUpdate() gameObject.SetActive(false); return; } + if (UI.BDArmorySetup.GameIsPaused) + { + if (audioSource.isPlaying) + { + audioSource.Stop(); + } + return; + } + } + IEnumerator DelayBlastSFX(float delay) + { + if (delay > 0) + { + yield return new WaitForSeconds(delay); + } + audioSource.PlayOneShot(ExSound); } - static void CreateObjectPool(string ModelPath, string soundPath) { var key = ModelPath + soundPath; @@ -120,19 +164,10 @@ static void CreateObjectPool(string ModelPath, string soundPath) var eFx = FXTemplate.AddComponent(); if (!String.IsNullOrEmpty(soundPath)) { - var soundClip = GameDatabase.Instance.GetAudioClip(soundPath); - if (soundClip == null) - { - Debug.LogError("[BDArmory.FXBase]: " + soundPath + " was not found, using the default sound instead. Please fix your model."); - soundClip = GameDatabase.Instance.GetAudioClip(defaultSoundPath); - } - - eFx.ExSound = soundClip; eFx.audioSource = FXTemplate.AddComponent(); eFx.audioSource.minDistance = 200; eFx.audioSource.maxDistance = 5500; eFx.audioSource.spatialBlend = 1; - } FXTemplate.SetActive(false); FXPools[key] = ObjectPool.CreateObjectPool(FXTemplate, 10, true, true, 0f, false); @@ -178,6 +213,7 @@ static void CreateObjectPool(string ModelPath, string soundPath) eFx.audioSource.maxDistance = 3000; eFx.audioSource.priority = 9999; } + eFx.SoundPath = soundPath; } newFX.SetActive(true); } diff --git a/BDArmory/FX/FireFX.cs b/BDArmory/FX/FireFX.cs index c57586623..d7ac5abfa 100644 --- a/BDArmory/FX/FireFX.cs +++ b/BDArmory/FX/FireFX.cs @@ -1,20 +1,23 @@ using System; +using System.Linq; +using UnityEngine; + using BDArmory.Competition; -using BDArmory.Core; -using BDArmory.Core.Extension; -using BDArmory.Core.Utils; -using BDArmory.Core.Module; -using BDArmory.Misc; +using BDArmory.Damage; +using BDArmory.Extensions; using BDArmory.Modules; +using BDArmory.Settings; using BDArmory.UI; -using System.Linq; -using UnityEngine; +using BDArmory.Utils; namespace BDArmory.FX { class FireFX : MonoBehaviour { Part parentPart; + // string parentPartName = ""; + // string parentVesselName = ""; + public static ObjectPool CreateFireFXPool(string modelPath) { var template = GameDatabase.Instance.GetModel(modelPath); @@ -37,7 +40,7 @@ public static ObjectPool CreateFireFXPool(string modelPath) public string SourceVessel; private string explModelPath = "BDArmory/Models/explosion/explosion"; private string explSoundPath = "BDArmory/Sounds/explode1"; - int explosionLayerMask = (int)(LayerMasks.Parts | LayerMasks.Scenery | LayerMasks.EVA | LayerMasks.Unknown19 | LayerMasks.Unknown23); // Why 19 and 23? + const int explosionLayerMask = (int)(LayerMasks.Parts | LayerMasks.Scenery | LayerMasks.EVA | LayerMasks.Unknown19 | LayerMasks.Unknown23 | LayerMasks.Wheels); // Why 19 and 23? bool parentBeingDestroyed = false; PartResource fuel; @@ -51,6 +54,9 @@ public static ObjectPool CreateFireFXPool(string modelPath) // bool lookedForEngine = false; KSPParticleEmitter[] pEmitters; + + Collider[] blastHitColliders = new Collider[100]; + void OnEnable() { if (parentPart == null) @@ -58,7 +64,7 @@ void OnEnable() gameObject.SetActive(false); return; } - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log($"[BDArmory.FireFX]: Fire added to {parentPart.name}" + (parentPart.vessel != null ? $" on {parentPart.vessel.vesselName}" : "")); + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log($"[BDArmory.FireFX]: Fire added to {parentPart.name}" + (parentPart.vessel != null ? $" on {parentPart.vessel.vesselName}" : "")); hasFuel = true; tntMassEquivalent = 0; startTime = Time.time; @@ -121,7 +127,7 @@ void OnEnable() enginerestartTime = Time.time; } burnTime = 4; - Utils.RefreshAssociatedWindows(parentPart); + GUIUtils.RefreshAssociatedWindows(parentPart); Debug.Log("[FireFX] firebottles remaining in " + parentPart.name + ": " + FBX.FireBottles); } else @@ -146,13 +152,16 @@ void OnEnable() void OnDisable() { // Clean up emitters. - BDArmorySetup.numberOfParticleEmitters--; - foreach (var pe in pEmitters) - if (pe != null) - { - pe.emit = false; - EffectBehaviour.RemoveParticleEmitter(pe); - } + if (pEmitters is not null) + { + --BDArmorySetup.numberOfParticleEmitters; + foreach (var pe in pEmitters) + if (pe != null) + { + pe.emit = false; + EffectBehaviour.RemoveParticleEmitter(pe); + } + } // Clean up part and resource references. parentPart = null; Seat = null; @@ -166,7 +175,7 @@ void OnDisable() fireIntensity = 1; } - void Update() + void FixedUpdate() { if (!gameObject.activeInHierarchy || !HighLogic.LoadedSceneIsFlight || BDArmorySetup.GameIsPaused) { @@ -187,7 +196,7 @@ void Update() { if (isSRB) { - if (parentPart.RequestResource("SolidFuel", (double)(burnRate * TimeWarp.deltaTime)) <= 0) + if (parentPart.RequestResource("SolidFuel", (double)(burnRate * TimeWarp.fixedDeltaTime)) <= 0) { hasFuel = false; } @@ -210,7 +219,7 @@ void Update() { if (engine.EngineIgnited) { - if (parentPart.RequestResource("LiquidFuel", (double)(burnRate * TimeWarp.deltaTime)) <= 0) + if (parentPart.RequestResource("LiquidFuel", (double)(burnRate * TimeWarp.fixedDeltaTime)) <= 0) { hasFuel = false; } @@ -235,7 +244,7 @@ void Update() { if (fuel.amount > (fuel.maxAmount * 0.15f) || (fuel.amount > 0 && fuel.amount < (fuel.maxAmount * 0.10f))) { - fireIntensity = (burnRate * Mathf.Clamp((float)((1 - (fuel.amount / fuel.maxAmount)) * 4), 0.1f * BDArmorySettings.BD_TANK_LEAK_RATE, 4 * BDArmorySettings.BD_TANK_LEAK_RATE) * TimeWarp.deltaTime); + fireIntensity = (burnRate * Mathf.Clamp((float)((1 - (fuel.amount / fuel.maxAmount)) * 4), 0.1f * BDArmorySettings.BD_TANK_LEAK_RATE, 4 * BDArmorySettings.BD_TANK_LEAK_RATE) * TimeWarp.fixedDeltaTime); fuel.amount -= fireIntensity; burnScale = Mathf.Clamp((float)((1 - (fuel.amount / fuel.maxAmount)) * 4), 0.1f * BDArmorySettings.BD_TANK_LEAK_RATE, 2 * BDArmorySettings.BD_TANK_LEAK_RATE); } @@ -257,7 +266,7 @@ void Update() if (ox.amount > 0) { fireIntensity *= 1.2f; - ox.amount -= (burnRate * Mathf.Clamp((float)((1 - (ox.amount / ox.maxAmount)) * 4), 0.1f * BDArmorySettings.BD_TANK_LEAK_RATE, 4 * BDArmorySettings.BD_TANK_LEAK_RATE) * TimeWarp.deltaTime); + ox.amount -= (burnRate * Mathf.Clamp((float)((1 - (ox.amount / ox.maxAmount)) * 4), 0.1f * BDArmorySettings.BD_TANK_LEAK_RATE, 4 * BDArmorySettings.BD_TANK_LEAK_RATE) * TimeWarp.fixedDeltaTime); } else { @@ -269,7 +278,7 @@ void Update() { if (mp.amount > (mp.maxAmount * 0.15f) || (mp.amount > 0 && mp.amount < (mp.maxAmount * 0.10f))) { - mp.amount -= (burnRate * Mathf.Clamp((float)((1 - (mp.amount / mp.maxAmount)) * 4), 0.1f * BDArmorySettings.BD_TANK_LEAK_RATE, 4 * BDArmorySettings.BD_TANK_LEAK_RATE) * TimeWarp.deltaTime); + mp.amount -= (burnRate * Mathf.Clamp((float)((1 - (mp.amount / mp.maxAmount)) * 4), 0.1f * BDArmorySettings.BD_TANK_LEAK_RATE, 4 * BDArmorySettings.BD_TANK_LEAK_RATE) * TimeWarp.fixedDeltaTime); if (burnScale < 0) { burnScale = Mathf.Clamp((float)((1 - (mp.amount / mp.maxAmount)) * 4), 0.1f * BDArmorySettings.BD_TANK_LEAK_RATE, 2 * BDArmorySettings.BD_TANK_LEAK_RATE); @@ -324,9 +333,9 @@ void Update() { parentPart.temperature += burnRate * Mathf.Clamp((float)((1 - (mp.amount / mp.maxAmount)) * 4), 0.1f * BDArmorySettings.BD_TANK_LEAK_RATE, 4 * BDArmorySettings.BD_TANK_LEAK_RATE) * Time.deltaTime; } - else if (ec != null || ox != null) + else //if (ec != null || ox != null) { - parentPart.temperature += burnRate * BDArmorySettings.BD_FIRE_DAMAGE * Time.deltaTime; + parentPart.temperature += burnRate * BDArmorySettings.BD_FIRE_DAMAGE * Time.fixedDeltaTime; } } } @@ -334,22 +343,22 @@ void Update() { if (BDArmorySettings.BD_INTENSE_FIRES) { - parentPart.AddDamage(fireIntensity * BDArmorySettings.BD_FIRE_DAMAGE * Time.deltaTime); + parentPart.AddDamage(fireIntensity * BDArmorySettings.BD_FIRE_DAMAGE * Time.fixedDeltaTime); } else { if (BDArmorySettings.ENABLE_HOS && BDArmorySettings.HALL_OF_SHAME_LIST.Contains(parentPart.vessel.GetName())) { - parentPart.AddDamage(BDArmorySettings.HOS_FIRE * Time.deltaTime); + parentPart.AddDamage(BDArmorySettings.HOS_FIRE * Time.fixedDeltaTime); } else - parentPart.AddDamage(BDArmorySettings.BD_FIRE_DAMAGE * Time.deltaTime); + parentPart.AddDamage(BDArmorySettings.BD_FIRE_DAMAGE * Time.fixedDeltaTime); } - BDACompetitionMode.Instance.Scores.RegisterBattleDamage(SourceVessel, parentPart.vessel, BDArmorySettings.BD_FIRE_DAMAGE * Time.deltaTime); + BDACompetitionMode.Instance.Scores.RegisterBattleDamage(SourceVessel, parentPart.vessel, BDArmorySettings.BD_FIRE_DAMAGE * Time.fixedDeltaTime); } } - if (disableTime < 0 && ((!hasFuel && burnTime < 0)|| (burnTime >= 0 && Time.time - startTime > burnTime))) + if (disableTime < 0 && ((!hasFuel && burnTime < 0) || (burnTime >= 0 && Time.time - startTime > burnTime))) { disableTime = Time.time; //grab time when emission stops foreach (var pe in pEmitters) @@ -364,9 +373,9 @@ void Update() pe.maxSize = burnScale * 1.2f; } } - if (surfaceFire && parentPart.vessel.horizontalSrfSpeed > 120) //blow out surface fires if moving fast enough + if (surfaceFire && parentPart.vessel.horizontalSrfSpeed > 120 && SourceVessel != "GM") //blow out surface fires if moving fast enough { - burnTime = 5; //only fuel+oxy or monoprop fires in vac/non-oxy atmo + burnTime = 5; } // Note: the following can set the parentPart to null. if (disableTime > 0 && Time.time - disableTime > _highestEnergy) //wait until last emitted particle has finished @@ -377,6 +386,10 @@ void Update() { Deactivate(); //only fuel+oxy or monoprop fires in vac/non-oxy atmo } + if (FlightGlobals.getAltitudeAtPos(transform.position) <= 0) + { + Deactivate(); //don't burn underwater + } } void Detonate() @@ -424,17 +437,23 @@ void Detonate() ec.maxAmount = 0; ec.isVisible = false; if (!parentBeingDestroyed) parentPart.RemoveResource(ec);//destroy battery. not calling part.destroy, since some batteries in cockpits. - Utils.RefreshAssociatedWindows(parentPart); + GUIUtils.RefreshAssociatedWindows(parentPart); } //tntMassEquivilent *= BDArmorySettings.BD_AMMO_DMG_MULT; //handled by EXP_DMG_MOD_BATTLE_DAMAGE - if (BDArmorySettings.DRAW_DEBUG_LABELS && tntMassEquivalent > 0) + if (BDArmorySettings.DEBUG_OTHER && tntMassEquivalent > 0) { Debug.Log("[BDArmory.FireFX]: Fuel Explosion in " + this.parentPart.name + ", TNT mass equivalent " + tntMassEquivalent + $" (Fuel: {tntFuel / 6f}, Ox: {tntOx / 6f}, MP: {tntMP / 6f}, EC: {tntEC})"); } if (excessFuel) { float blastRadius = BlastPhysicsUtils.CalculateBlastRange(tntMassEquivalent); - using (var blastHits = Physics.OverlapSphere(parentPart.transform.position, blastRadius, explosionLayerMask).AsEnumerable().GetEnumerator()) + var hitCount = Physics.OverlapSphereNonAlloc(parentPart.transform.position, blastRadius, blastHitColliders, explosionLayerMask); + if (hitCount == blastHitColliders.Length) + { + blastHitColliders = Physics.OverlapSphere(parentPart.transform.position, blastRadius, explosionLayerMask); + hitCount = blastHitColliders.Length; + } + using (var blastHits = blastHitColliders.Take(hitCount).GetEnumerator()) while (blastHits.MoveNext()) { if (blastHits.Current == null) continue; @@ -457,7 +476,7 @@ void Detonate() if (p == partHit) { BulletHitFX.AttachFire(hit.point, p, 1, SourceVessel, BDArmorySettings.WEAPON_FX_DURATION * (1 - (distToG0.magnitude / blastRadius)), 1, true); - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_OTHER) { Debug.Log("[BDArmory.FireFX]: " + this.parentPart.name + " hit by burning fuel"); } @@ -490,38 +509,82 @@ void Detonate() public void AttachAt(Part hitPart, Vector3 hit, Vector3 offset, string sourcevessel) { - if (hitPart == null) return; + if (hitPart is null) return; parentPart = hitPart; + // parentPartName = parentPart.name; + // parentVesselName = parentPart.vessel.vesselName; transform.SetParent(hitPart.transform); transform.position = hit + offset; transform.rotation = Quaternion.FromToRotation(Vector3.up, -FlightGlobals.getGeeForceAtPosition(transform.position)); parentPart.OnJustAboutToDie += OnParentDestroy; parentPart.OnJustAboutToBeDestroyed += OnParentDestroy; + if ((Versioning.version_major == 1 && Versioning.version_minor > 10) || Versioning.version_major > 1) // onVesselUnloaded event introduced in 1.11 + OnVesselUnloaded_1_11(true); // Catch unloading events too. SourceVessel = sourcevessel; gameObject.SetActive(true); } public void OnParentDestroy() { - if (parentPart != null) + if (parentPart is not null) { parentBeingDestroyed = true; parentPart.OnJustAboutToDie -= OnParentDestroy; parentPart.OnJustAboutToBeDestroyed -= OnParentDestroy; if (!surfaceFire) Detonate(); - else Deactivate(); + Deactivate(); } } + public void OnVesselUnloaded(Vessel vessel) + { + if (parentPart is not null && (parentPart.vessel is null || parentPart.vessel == vessel)) + { + OnParentDestroy(); + } + else if (parentPart is null) + { + Deactivate(); // Sometimes (mostly when unloading a vessel) the parent becomes null without triggering OnParentDestroy. + } + } + + void OnVesselUnloaded_1_11(bool addRemove) // onVesselUnloaded event introduced in 1.11 + { + if (addRemove) + GameEvents.onVesselUnloaded.Add(OnVesselUnloaded); + else + GameEvents.onVesselUnloaded.Remove(OnVesselUnloaded); + } + void Deactivate() { - if (gameObject.activeInHierarchy) + if (gameObject is not null && gameObject.activeSelf) // Deactivate even if a parent is already inactive. { disableTime = -1; parentPart = null; transform.parent = null; // Detach ourselves from the parent transform so we don't get destroyed when it does. gameObject.SetActive(false); } + if ((Versioning.version_major == 1 && Versioning.version_minor > 10) || Versioning.version_major > 1) // onVesselUnloaded event introduced in 1.11 + OnVesselUnloaded_1_11(false); + } + + void OnDestroy() // This shouldn't be happening except on exiting KSP, but sometimes they get destroyed instead of disabled! + { + // if (HighLogic.LoadedSceneIsFlight) Debug.LogError($"[BDArmory.FireFX]: FireFX on {parentPartName} ({parentVesselName}) was destroyed!"); + // Clean up emitters. + if (pEmitters is not null && pEmitters.Any(pe => pe is not null)) + { + BDArmorySetup.numberOfParticleEmitters--; + foreach (var pe in pEmitters) + if (pe != null) + { + pe.emit = false; + EffectBehaviour.RemoveParticleEmitter(pe); + } + } + if ((Versioning.version_major == 1 && Versioning.version_minor > 10) || Versioning.version_major > 1) // onVesselUnloaded event introduced in 1.11 + OnVesselUnloaded_1_11(false); } } } diff --git a/BDArmory/FX/FuelLeakFX.cs b/BDArmory/FX/FuelLeakFX.cs index f0bf7510b..2ecadae4b 100644 --- a/BDArmory/FX/FuelLeakFX.cs +++ b/BDArmory/FX/FuelLeakFX.cs @@ -1,19 +1,17 @@ -using BDArmory.Core; -using BDArmory.Misc; -using BDArmory.UI; -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Linq; using UnityEngine; +using BDArmory.Settings; +using BDArmory.UI; +using BDArmory.Utils; + namespace BDArmory.FX { class FuelLeakFX : MonoBehaviour { Part parentPart; + // string parentPartName = ""; + // string parentVesselName = ""; public static ObjectPool CreateLeakFXPool(string modelPath) { var template = GameDatabase.Instance.GetModel(modelPath); @@ -35,6 +33,7 @@ public static ObjectPool CreateLeakFXPool(string modelPath) ModuleEngines engine; private bool isSRB = false; KSPParticleEmitter[] pEmitters; + void OnEnable() { if (parentPart == null) @@ -42,7 +41,7 @@ void OnEnable() gameObject.SetActive(false); return; } - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log($"[BDArmory.LeakFX]: Leak added to {parentPart.name}" + (parentPart.vessel != null ? $" on {parentPart.vessel.vesselName}" : "")); + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log($"[BDArmory.LeakFX]: Leak added to {parentPart.name}" + (parentPart.vessel != null ? $" on {parentPart.vessel.vesselName}" : "")); engine = parentPart.FindModuleImplementing(); var solid = parentPart.Resources.Where(pr => pr.resourceName == "SolidFuel").FirstOrDefault(); @@ -67,6 +66,7 @@ void OnEnable() EffectBehaviour.AddParticleEmitter(pe.Current); } } + void OnDisable() { if (pEmitters != null) // Getting enabled when the parent part is null immediately disables it again before setting any of this up. @@ -85,7 +85,8 @@ void OnDisable() mp = null; drainRate = 1; } - void Update() + + void FixedUpdate() { if (!gameObject.activeInHierarchy || !HighLogic.LoadedSceneIsFlight || BDArmorySetup.GameIsPaused) { @@ -103,7 +104,7 @@ void Update() { if (fuel.amount > 0) { - parentPart.RequestResource("LiquidFuel", (double)(drainRate * Time.deltaTime)); + parentPart.RequestResource("LiquidFuel", (double)(drainRate * Time.fixedDeltaTime)); fuelLeft++; } } @@ -117,7 +118,7 @@ void Update() { //part.RequestResource("LiquidFuel", ((double)drainRate * Mathf.Clamp((float)fuel.amount, 40, 400) / Mathf.Clamp((float)fuel.maxAmount, 400, (float)fuel.maxAmount)) * Time.deltaTime); //This draining from across vessel? Trying alt method - fuel.amount -= ((double)drainRate * Mathf.Clamp((float)fuel.amount, 40, 400) / Mathf.Clamp((float)fuel.maxAmount, 400, (float)fuel.maxAmount)) * Time.deltaTime; + fuel.amount -= ((double)drainRate * Mathf.Clamp((float)fuel.amount, 40, 400) / Mathf.Clamp((float)fuel.maxAmount, 400, (float)fuel.maxAmount)) * Time.fixedDeltaTime; fuel.amount = Mathf.Clamp((float)fuel.amount, 0, (float)fuel.maxAmount); fuelLeft++; } @@ -129,7 +130,7 @@ void Update() { //part.RequestResource("Oxidizer", ((double)drainRate * Mathf.Clamp((float)ox.amount, 40, 400) / Mathf.Clamp((float)ox.maxAmount, 400, (float)ox.maxAmount) ) * Time.deltaTime); //more fuel = higher pressure, clamped at 400 since flow rate is constrained by outlet aperture, not fluid pressure - ox.amount -= ((double)drainRate * Mathf.Clamp((float)fuel.amount, 40, 400) / Mathf.Clamp((float)fuel.maxAmount, 400, (float)fuel.maxAmount)) * Time.deltaTime; + ox.amount -= ((double)drainRate * Mathf.Clamp((float)fuel.amount, 40, 400) / Mathf.Clamp((float)fuel.maxAmount, 400, (float)fuel.maxAmount)) * Time.fixedDeltaTime; ox.amount = Mathf.Clamp((float)ox.amount, 0, (float)ox.maxAmount); fuelLeft++; } @@ -140,7 +141,7 @@ void Update() if (mp.amount >= 0) { //part.RequestResource("MonoPropellant", ((double)drainRate * Mathf.Clamp((float)mp.amount, 40, 400) / Mathf.Clamp((float)mp.maxAmount, 400, (float)mp.maxAmount)) * Time.deltaTime); - mp.amount -= ((double)drainRate * Mathf.Clamp((float)mp.amount, 40, 400) / Mathf.Clamp((float)mp.maxAmount, 400, (float)mp.maxAmount)) * Time.deltaTime; + mp.amount -= ((double)drainRate * Mathf.Clamp((float)mp.amount, 40, 400) / Mathf.Clamp((float)mp.maxAmount, 400, (float)mp.maxAmount)) * Time.fixedDeltaTime; mp.amount = Mathf.Clamp((float)mp.amount, 0, (float)mp.maxAmount); fuelLeft++; } @@ -162,35 +163,82 @@ void Update() Deactivate(); } } + public void AttachAt(Part hitPart, RaycastHit hit, Vector3 offset) { - if (hitPart == null) return; + if (hitPart is null) return; parentPart = hitPart; + // parentPartName = parentPart.name; + // parentVesselName = parentPart.vessel.vesselName; transform.SetParent(hitPart.transform); transform.position = hit.point + offset; transform.rotation = Quaternion.FromToRotation(Vector3.up, -FlightGlobals.getGeeForceAtPosition(transform.position)); parentPart.OnJustAboutToDie += OnParentDestroy; parentPart.OnJustAboutToBeDestroyed += OnParentDestroy; + if ((Versioning.version_major == 1 && Versioning.version_minor > 10) || Versioning.version_major > 1) // onVesselUnloaded event introduced in 1.11 + OnVesselUnloaded_1_11(true); // Catch unloading events too. gameObject.SetActive(true); } - public void OnParentDestroy() + + void OnParentDestroy() { - if (parentPart) + if (parentPart is not null) { parentPart.OnJustAboutToDie -= OnParentDestroy; parentPart.OnJustAboutToBeDestroyed -= OnParentDestroy; Deactivate(); } } + + void OnVesselUnloaded(Vessel vessel) + { + if (parentPart is not null && (parentPart.vessel is null || parentPart.vessel == vessel)) + { + OnParentDestroy(); + } + else if (parentPart is null) + { + Deactivate(); // Sometimes (mostly when unloading a vessel) the parent becomes null without triggering OnParentDestroy. + } + } + + void OnVesselUnloaded_1_11(bool addRemove) // onVesselUnloaded event introduced in 1.11 + { + if (addRemove) + GameEvents.onVesselUnloaded.Add(OnVesselUnloaded); + else + GameEvents.onVesselUnloaded.Remove(OnVesselUnloaded); + } + void Deactivate() { - if (gameObject.activeInHierarchy) + if (gameObject is not null && gameObject.activeSelf) // Deactivate even if a parent is already inactive. { disableTime = -1; parentPart = null; transform.parent = null; // Detach ourselves from the parent transform so we don't get destroyed if it does. gameObject.SetActive(false); } + if ((Versioning.version_major == 1 && Versioning.version_minor > 10) || Versioning.version_major > 1) // onVesselUnloaded event introduced in 1.11 + OnVesselUnloaded_1_11(false); + } + + void OnDestroy() // This shouldn't be happening except on exiting KSP, but sometimes they get destroyed instead of disabled! + { + // if (HighLogic.LoadedSceneIsFlight) Debug.LogError($"[BDArmory.FuelLeakFX]: FuelLeakFX on {parentPartName} ({parentVesselName}) was destroyed!"); + // Clean up emitters. + if (pEmitters is not null && pEmitters.Any(pe => pe is not null)) + { + BDArmorySetup.numberOfParticleEmitters--; + foreach (var pe in pEmitters) + if (pe != null) + { + pe.emit = false; + EffectBehaviour.RemoveParticleEmitter(pe); + } + } + if ((Versioning.version_major == 1 && Versioning.version_minor > 10) || Versioning.version_major > 1) // onVesselUnloaded event introduced in 1.11 + OnVesselUnloaded_1_11(false); } } } diff --git a/BDArmory/FX/NukeFX.cs b/BDArmory/FX/NukeFX.cs index 0efac0a20..171b7f2fd 100644 --- a/BDArmory/FX/NukeFX.cs +++ b/BDArmory/FX/NukeFX.cs @@ -4,18 +4,19 @@ using UnityEngine; using BDArmory.Competition; -using BDArmory.Core.Extension; -using BDArmory.Core.Utils; -using BDArmory.Core; +using BDArmory.Damage; +using BDArmory.Extensions; using BDArmory.GameModes; -using BDArmory.Misc; -using BDArmory.Modules; +using BDArmory.Settings; +using BDArmory.Utils; +using BDArmory.Weapons; namespace BDArmory.FX { public class NukeFX : MonoBehaviour { public static Dictionary nukePool = new Dictionary(); + public static Dictionary audioClips = new Dictionary(); // Pool the audio clips separately. private bool hasDetonated = false; private float startTime; @@ -26,7 +27,7 @@ public class NukeFX : MonoBehaviour public Light LightFx { get; set; } public KSPParticleEmitter[] pEmitters { get; set; } public float StartTime { get; set; } - public AudioClip ExSound { get; set; } + public string SoundPath { get; set; } public AudioSource audioSource { get; set; } public float thermalRadius { get; set; } //clamped blast range public float fluence { get; set; } //thermal magnitude @@ -59,11 +60,11 @@ public class NukeFX : MonoBehaviour private float EMPRadius = 100; private float scale = 1; - int explosionLayerMask = (int)(LayerMasks.Parts | LayerMasks.Scenery | LayerMasks.EVA | LayerMasks.Unknown19 | LayerMasks.Unknown23); // Why 19 and 23? + const int explosionLayerMask = (int)(LayerMasks.Parts | LayerMasks.Scenery | LayerMasks.EVA | LayerMasks.Unknown19 | LayerMasks.Unknown23 | LayerMasks.Wheels); // Why 19 and 23? static RaycastHit[] lineOfSightHits; static RaycastHit[] reverseHits; - static Collider[] overlapSphereColliders; + Collider[] blastHitColliders = new Collider[100]; public static List IgnoreParts; public static List IgnoreBuildings; internal static readonly float ExplosionVelocity = 422.75f; @@ -74,7 +75,6 @@ void Awake() { if (lineOfSightHits == null) { lineOfSightHits = new RaycastHit[100]; } if (reverseHits == null) { reverseHits = new RaycastHit[100]; } - if (overlapSphereColliders == null) { overlapSphereColliders = new Collider[100]; } if (IgnoreParts == null) { IgnoreParts = new List(); } if (IgnoreBuildings == null) { IgnoreBuildings = new List(); } } @@ -82,11 +82,11 @@ void Awake() private void OnEnable() { StartTime = Time.time; - MaxTime = Mathf.Sqrt((thermalRadius / ExplosionVelocity) * 3f) * 2f; // Scale MaxTime to get a reasonable visualisation of the explosion. - scale = Mathf.Sqrt(400 * (6 * yield)) / 219; - if (BDArmorySettings.DRAW_DEBUG_LABELS) + MaxTime = BDAMath.Sqrt((thermalRadius / ExplosionVelocity) * 3f) * 2f; // Scale MaxTime to get a reasonable visualisation of the explosion. + scale = BDAMath.Sqrt(400 * (6 * yield)) / 219; + if (BDArmorySettings.DEBUG_DAMAGE) { - Debug.Log("[BDArmory.NukeFX]: Explosion started tntMass: {" + yield + "} BlastRadius: {" + thermalRadius + "} StartTime: {" + StartTime + "}, Duration: {" + MaxTime + "}"); + Debug.Log($"[BDArmory.NukeFX]: Explosion started! yield: {yield} BlastRadius: {thermalRadius} StartTime: {StartTime}, Duration: {MaxTime}"); } if (HighLogic.LoadedSceneIsFlight) { @@ -98,16 +98,16 @@ private void OnEnable() FlightGlobals.getExternalTemperature(transform.position)); hasDetonated = false; - //EMP output increases as the sqrt of yield (determined power) and prompt gamma output (~0.5% of yield) + //EMP output increases as the sqrt of yield (determined power) and prompt gamma output (~0.5% of yield) //srf detonation is capped to about 16km, < 10km alt electrons qucikly absorbed by atmo. //above 10km, emp radius can easily reach 100s of km. But that's no fun, so... if (FlightGlobals.getAltitudeAtPos(transform.position) < 10000) { - EMPRadius = Mathf.Sqrt(yield) * 100; + EMPRadius = BDAMath.Sqrt(yield) * 500; } else { - EMPRadius = Mathf.Sqrt(yield) * 1000; + EMPRadius = BDAMath.Sqrt(yield) * 1000; } pEmitters = gameObject.GetComponentsInChildren(); @@ -121,6 +121,11 @@ private void OnEnable() } LightFx = gameObject.GetComponent(); LightFx.range = 0; + audioSource = gameObject.GetComponent(); + if (!string.IsNullOrEmpty(SoundPath)) + { + audioSource.PlayOneShot(audioClips[SoundPath]); + } } } @@ -150,10 +155,6 @@ private void CalculateBlastEvents() { if (enuEvents.Current == null) continue; - if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - Debug.Log("[BDArmory.ExplosionFX]: Enqueueing Blast Event"); - } explosionEvents.Enqueue(enuEvents.Current); } } @@ -166,7 +167,13 @@ private List ProcessingBlastSphere() explosionEventsBuildingAdded.Clear(); explosionEventsVesselsHit.Clear(); - using (var hitCollidersEnu = Physics.OverlapSphere(Position, thermalRadius * 2, explosionLayerMask).AsEnumerable().GetEnumerator()) + var hitCount = Physics.OverlapSphereNonAlloc(Position, thermalRadius * 2f, blastHitColliders, explosionLayerMask); + if (hitCount == blastHitColliders.Length) + { + blastHitColliders = Physics.OverlapSphere(Position, thermalRadius * 2f, explosionLayerMask); + hitCount = blastHitColliders.Length; + } + using (var hitCollidersEnu = blastHitColliders.Take(hitCount).GetEnumerator()) { while (hitCollidersEnu.MoveNext()) { @@ -179,7 +186,7 @@ private List ProcessingBlastSphere() if (ProjectileUtils.IsIgnoredPart(partHit)) continue; // Ignore ignored parts. if (partHit.mass > 0 && !explosionEventsPartsAdded.Contains(partHit)) { - var damaged = ProcessPartEvent(partHit, SourceVesselName, explosionEventsPreProcessing, explosionEventsPartsAdded); + var damaged = ProcessPartEvent(partHit, SourceVesselName, explosionEventsPreProcessing, explosionEventsPartsAdded); } } else @@ -195,7 +202,7 @@ private List ProcessingBlastSphere() RaycastHit rayHit; if (Physics.Raycast(ray, out rayHit, thermalRadius, explosionLayerMask)) { - DestructibleBuilding destructibleBuilding = rayHit.collider.gameObject.GetComponentUpwards(); + //DestructibleBuilding destructibleBuilding = rayHit.collider.gameObject.GetComponentUpwards(); distance = Vector3.Distance(Position, rayHit.point); if (building.IsIntact) @@ -208,8 +215,9 @@ private List ProcessingBlastSphere() } } } - catch + catch (Exception e) { + Debug.LogError($"[BDArmory.NukeFX]: Exception in overlapSphere collider processing: {e.Message}\n{e.StackTrace}"); } } } @@ -254,9 +262,28 @@ private bool ProcessPartEvent(Part part, string sourceVesselName, List(); Part p = eva ? eva.part : hit.collider.gameObject.GetComponentInParent(); - if (p == part) + if (lastValidAtmDensity < 0.1) { - eventList.Add(new PartNukeHitEvent() + if (p == part) //if exoatmo, impulse/thermal bloom only to parts in LoS + { + eventList.Add(new PartNukeHitEvent() + { + Distance = distToG0, + Part = part, + TimeToImpact = distToG0 / ExplosionVelocity, + HitPoint = hit.point, + Hit = hit, + SourceVesselName = sourceVesselName, + }); + + partsAdded.Add(part); + return true; + } + return false; + } + else + { + eventList.Add(new PartNukeHitEvent() //else everything heated/hit by shockwave { Distance = distToG0, Part = part, @@ -269,8 +296,8 @@ private bool ProcessPartEvent(Part part, string sourceVesselName, List detonationTimer) + if (hasDetonated) { - if (!hasDetonated) + if (LightFx != null) LightFx.intensity -= 12 * scale * Time.deltaTime; + if (TimeIndex > 0.3f && pEmitters != null) // 0.3s seems to be enough to always show the explosion, but 0.2s isn't for some reason. { - hasDetonated = true; - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("{BDArmory.NukeFX] Beginning detonation"); - CalculateBlastEvents(); - - LightFx = gameObject.GetComponent(); - LightFx.range = thermalRadius; - if (lastValidAtmDensity < 0.05) - { - FXEmitter.CreateFX(transform.position, scale, flashModelPath, "", 0.3f, 0.3f); - } - else + if (TimeIndex > 0.3f && pEmitters != null) // 0.3s seems to be enough to always show the explosion, but 0.2s isn't for some reason. { - //default model scaled for 20kt; yield = 20 = scale of 1 - //scaling calc is roughly SqRt( 400 * (6x)) - if (!string.IsNullOrWhiteSpace(flashModelPath)) - FXEmitter.CreateFX(transform.position, scale, flashModelPath, blastSoundPath, 0.3f, -1, default, true); - if (!string.IsNullOrWhiteSpace(shockModelPath)) - FXEmitter.CreateFX(transform.position, scale * lastValidAtmDensity, shockModelPath, blastSoundPath, 0.3f, -1, default, true); - if (!string.IsNullOrWhiteSpace(blastModelPath)) - FXEmitter.CreateFX(transform.position, scale, blastModelPath, blastSoundPath, 1.5f, Mathf.Clamp(30 * scale, 30f, 90f), default, true); - } - if (Utils.GetRadarAltitudeAtPos(transform.position) < 200 * scale) - { - double latitudeAtPos = FlightGlobals.currentMainBody.GetLatitude(transform.position); - double longitudeAtPos = FlightGlobals.currentMainBody.GetLongitude(transform.position); - double altitude = FlightGlobals.currentMainBody.TerrainAltitude(latitudeAtPos, longitudeAtPos); - if (!string.IsNullOrWhiteSpace(plumeModelPath)) - FXEmitter.CreateFX(FlightGlobals.currentMainBody.GetWorldSurfacePosition(latitudeAtPos, longitudeAtPos, altitude), Mathf.Clamp(scale, 0.01f, 3f), plumeModelPath, blastSoundPath, Mathf.Clamp(30 * scale, 30f, 90f), Mathf.Clamp(30 * scale, 30f, 90f), default, true, true); - if (!string.IsNullOrWhiteSpace(debrisModelPath)) - FXEmitter.CreateFX(FlightGlobals.currentMainBody.GetWorldSurfacePosition(latitudeAtPos, longitudeAtPos, altitude), scale, debrisModelPath, blastSoundPath, 1.5f, Mathf.Clamp(30 * scale, 30f, 90f), default, true) ; + foreach (var pe in pEmitters) + { + if (pe == null) continue; + pe.emit = false; + } } } - if (LightFx != null) LightFx.intensity -= 12 * scale * Time.deltaTime; - } - if (hasDetonated && TimeIndex > 0.3f && pEmitters != null) // 0.3s seems to be enough to always show the explosion, but 0.2s isn't for some reason. - { - foreach (var pe in pEmitters) - { - if (pe == null) continue; - pe.emit = false; - } } } } @@ -334,11 +330,49 @@ public void FixedUpdate() if (!gameObject.activeInHierarchy) return; //floating origin and velocity offloading corrections - if (!FloatingOrigin.Offset.IsZero() || !Krakensbane.GetFrameVelocity().IsZero()) + if (BDKrakensbane.IsActive) { - transform.position -= FloatingOrigin.OffsetNonKrakensbane; + transform.position -= BDKrakensbane.FloatingOriginOffsetNonKrakensbane; } + if (Time.time - startTime > detonationTimer) + { + if (!hasDetonated) + { + hasDetonated = true; + CalculateBlastEvents(); + LightFx = gameObject.GetComponent(); + LightFx.range = thermalRadius; + LightFx.intensity = thermalRadius / 3f; + if (lastValidAtmDensity < 0.05) + { + FXEmitter.CreateFX(transform.position, scale, flashModelPath, "", 0.3f); + } + else + { + //default model scaled for 20kt; yield = 20 = scale of 1 + //scaling calc is roughly SqRt( 400 * (6x)) + //fireball diameter is 59 * Mathf.Pow(yield, 0.4f), apparently? + if (!string.IsNullOrWhiteSpace(flashModelPath)) + FXEmitter.CreateFX(transform.position, scale, flashModelPath, "", 0.3f, -1, default, true); + if (!string.IsNullOrWhiteSpace(shockModelPath)) + FXEmitter.CreateFX(transform.position, scale * lastValidAtmDensity, shockModelPath, "", 0.3f, -1, default, true); + if (!string.IsNullOrWhiteSpace(blastModelPath)) + FXEmitter.CreateFX(transform.position, scale, blastModelPath, blastSoundPath, 1.5f, Mathf.Clamp(30 * scale, 30f, 90f), default, true); + + if (BodyUtils.GetRadarAltitudeAtPos(transform.position) < 200 * scale) + { + double latitudeAtPos = FlightGlobals.currentMainBody.GetLatitude(transform.position); + double longitudeAtPos = FlightGlobals.currentMainBody.GetLongitude(transform.position); + double altitude = FlightGlobals.currentMainBody.TerrainAltitude(latitudeAtPos, longitudeAtPos); + if (!string.IsNullOrWhiteSpace(plumeModelPath)) + FXEmitter.CreateFX(FlightGlobals.currentMainBody.GetWorldSurfacePosition(latitudeAtPos, longitudeAtPos, altitude), Mathf.Clamp(scale, 0.01f, 3f), plumeModelPath, "", Mathf.Clamp(30 * scale, 30f, 90f), Mathf.Clamp(30 * scale, 30f, 90f), default, true, true); + if (!string.IsNullOrWhiteSpace(debrisModelPath)) + FXEmitter.CreateFX(FlightGlobals.currentMainBody.GetWorldSurfacePosition(latitudeAtPos, longitudeAtPos, altitude), scale, debrisModelPath, "", 1.5f, Mathf.Clamp(30 * scale, 30f, 90f), default, true); + } + } + } + } if (hasDetonated) { while (explosionEvents.Count > 0 && explosionEvents.Peek().TimeToImpact <= TimeIndex) @@ -359,10 +393,6 @@ public void FixedUpdate() if (hasDetonated && explosionEvents.Count == 0 && TimeIndex > MaxTime) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) - { - Debug.Log("[BDArmory.NukeFX]: Explosion Finished"); - } gameObject.SetActive(false); return; } @@ -375,12 +405,12 @@ private void ExecuteBuildingBlastEvent(BuildingNukeHitEvent eventToExecute) if (building && building.IsIntact) { var distToEpicenter = Mathf.Max((transform.position - building.transform.position).magnitude, 1f); - var blastImpulse = Mathf.Pow(3.01f * 1100f / distToEpicenter, 1.25f) * 6.894f * lastValidAtmDensity * yieldCubeRoot; - //Debug.Log("[BDArmory.NukeFX]: Building hit; distToG0: " + distToEpicenter + ", yield: " + yield + ", building: " + building.name); + var blastImpulse = Mathf.Pow(3.01f * 1100f / distToEpicenter, 1.25f) * 6.894f * Mathf.Max(lastValidAtmDensity, 0.05f) * yieldCubeRoot; + // Debug.Log($"[BDArmory.NukeFX]: Building hit; distToG0: {distToEpicenter}, yield: {yield}, building: {building.name}, lastValidAtmDensity: {lastValidAtmDensity}, impulse: {blastImpulse}"); if (!double.IsNaN(blastImpulse)) //140kPa, level at which reinforced concrete structures are destroyed { - //Debug.Log("[BDArmory.NukeFX]: Building Impulse: " + blastImpulse); + // Debug.Log("[BDArmory.NukeFX]: Building Impulse: " + blastImpulse); if (blastImpulse > 140) { building.Demolish(); @@ -401,65 +431,32 @@ private void ExecutePartBlastEvent(PartNukeHitEvent eventToExecute) var vesselMass = part.vessel.totalMass; if (vesselMass == 0) vesselMass = part.mass; // Sometimes if the root part is the only part of the vessel, then part.vessel.totalMass is 0, despite the part.mass not being 0. float radiativeArea = !double.IsNaN(part.radiativeArea) ? (float)part.radiativeArea : part.GetArea(); - if (!BDArmorySettings.PAINTBALL_MODE) - { + if (!BDArmorySettings.PAINTBALL_MODE) + { if (!eventToExecute.IsNegativePressure) { - if (BDArmorySettings.DRAW_DEBUG_LABELS && double.IsNaN(part.radiativeArea)) + if (BDArmorySettings.DEBUG_DAMAGE && double.IsNaN(part.radiativeArea)) { - Debug.Log("[BDArmory.NukeFX]: radiative area of part " + part + " was NaN, using approximate area " + radiativeArea + " instead."); + Debug.Log($"[BDArmory.NukeFX]: radiative area of part {part} was NaN, using approximate area {radiativeArea} instead."); } - //part.skinTemperature += fluence * 3370000000 / (4 * Math.PI * (realDistance * realDistance)) * radiativeArea / 2; // Fluence scales linearly w/ yield, 1 Kt will produce between 33 TJ and 337 kJ at 0-1000m, - part.skinTemperature += fluence * (337000000 * BDArmorySettings.EXP_DMG_MOD_MISSILE) / (4 * Math.PI * (realDistance * realDistance)); // everything gets heated via atmosphere - if (isEMP) - { - if (part == part.vessel.rootPart) //don't apply EMP buildup per part - { - var EMP = part.vessel.rootPart.FindModuleImplementing(); - if (EMP == null) - { - EMP = (ModuleDrainEC)part.vessel.rootPart.AddModule("ModuleDrainEC"); - } - EMP.incomingDamage = ((EMPRadius / realDistance) * 100); //this way craft at edge of blast might only get disabled instead of bricked - //work on a better EMP damage value, in case of configs with very large thermalRadius - EMP.softEMP = false; - } - } - double blastImpulse = Mathf.Pow(3.01f * 1100f / realDistance, 1.25f) * 6.894f * lastValidAtmDensity * yieldCubeRoot; // * (radiativeArea / 3f); pascals/m isn't going to increase if a larger surface area, it's still going go be same force + double blastImpulse; + if (lastValidAtmDensity > 0.1) + blastImpulse = Mathf.Pow(3.01f * 1100f / realDistance, 1.25f) * 6.894f * lastValidAtmDensity * yieldCubeRoot; // * (radiativeArea / 3f); pascals/m isn't going to increase if a larger surface area, it's still going go be same force + else + blastImpulse = (part.mass * 15295.74) / (4 * Math.PI * Math.Pow(realDistance, 2.0)) * (part.radiativeArea / 3.0); if (blastImpulse > 0) { - if (rb != null && rb.mass > 0) - { - if (double.IsNaN(blastImpulse)) - { - Debug.LogWarning("[BDArmory.NukeFX]: blast impulse is NaN. distToG0: " + realDistance + ", vessel: " + part.vessel + ", atmDensity: " + lastValidAtmDensity + ", yield^(1/3): " + yieldCubeRoot + ", partHit: " + part + ", radiativeArea: " + radiativeArea); - } - else - { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.NukeTest]: Applying " + blastImpulse.ToString("0.0") + " impulse to " + part + " of mass " + part.mass + " at distance " + realDistance + "m"); - part.rb.AddForceAtPosition((part.transform.position - transform.position).normalized * ((float)blastImpulse * (radiativeArea / 3f)), part.transform.position, ForceMode.Impulse); - } - } - // Add Reverse Negative Event - explosionEvents.Enqueue(new PartNukeHitEvent() - { - Distance = thermalRadius - realDistance, - Part = part, - TimeToImpact = 2 * (thermalRadius / ExplosionVelocity) + (thermalRadius - realDistance) / ExplosionVelocity, - IsNegativePressure = true, - NegativeForce = (float)blastImpulse * 0.25f - }); float damage = 0; //float blastDamage = ((float)((yield * (45000000 * BDArmorySettings.EXP_DMG_MOD_MISSILE)) / (4f * Mathf.PI * realDistance * realDistance) * (radiativeArea / 2f))); //this shouldn't scale linearly float blastDamage = (float)blastImpulse; //* BDArmorySettings.EXP_DMG_MOD_MISSILE; //DMG_Mod is substantially increasing blast radius above what it should be if (float.IsNaN(blastDamage)) { - Debug.LogWarning("[BDArmory.NukeFX]: blast damage is NaN. distToG0: " + realDistance + ", yield: " + yield + ", part: " + part + ", radiativeArea: " + radiativeArea); + Debug.LogWarning($"[BDArmory.NukeFX]: blast damage is NaN. distToG0: {realDistance}, yield: {yield}, part: {part}, radiativeArea: {radiativeArea}"); } else { - if (!ProjectileUtils.CalculateExplosiveArmorDamage(part, blastImpulse, SourceVesselName, eventToExecute.Hit, ExplosionSource)) //false = armor blowthrough + if (!ProjectileUtils.CalculateExplosiveArmorDamage(part, blastImpulse, realDistance, SourceVesselName, eventToExecute.Hit, ExplosionSource, thermalRadius - realDistance)) //false = armor blowthrough { damage = part.AddExplosiveDamage(blastDamage, 1, ExplosionSource, 1); } @@ -502,11 +499,49 @@ private void ExecutePartBlastEvent(PartNukeHitEvent eventToExecute) } } } + + if (rb != null && rb.mass > 0) + { + if (double.IsNaN(blastImpulse)) + { + Debug.LogWarning($"[BDArmory.NukeFX]: blast impulse is NaN. distToG0: {realDistance}, vessel: {part.vessel}, atmDensity: {lastValidAtmDensity}, yield^(1/3): {yieldCubeRoot}, partHit: {part}, radiativeArea: {radiativeArea}"); + } + else + { + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log("[BDArmory.NukeFX]: Applying " + blastImpulse.ToString("0.0") + " impulse to " + part + " of mass " + part.mass + " at distance " + realDistance + "m"); + rb.AddForceAtPosition((part.transform.position - transform.position).normalized * ((float)blastImpulse * (radiativeArea / 3f)), part.transform.position, ForceMode.Impulse); + } + } + // Add Reverse Negative Event + explosionEvents.Enqueue(new PartNukeHitEvent() + { + Distance = thermalRadius - realDistance, + Part = part, + TimeToImpact = 2 * (thermalRadius / ExplosionVelocity) + (thermalRadius - realDistance) / ExplosionVelocity, + IsNegativePressure = true, + NegativeForce = (float)blastImpulse * 0.25f + }); } - else if (BDArmorySettings.DRAW_DEBUG_LABELS) + else if (BDArmorySettings.DEBUG_DAMAGE) { Debug.Log("[BDArmory.NukeFX]: Part " + part.name + " at distance " + realDistance + "m took no damage"); } + //part.skinTemperature += fluence * 3370000000 / (4 * Math.PI * (realDistance * realDistance)) * radiativeArea / 2; // Fluence scales linearly w/ yield, 1 Kt will produce between 33 TJ and 337 kJ at 0-1000m, + part.skinTemperature += (fluence * (337000000 * BDArmorySettings.EXP_DMG_MOD_MISSILE) / (4 * Math.PI * (realDistance * realDistance))); // everything gets heated via atmosphere + if (isEMP) + { + if (part == part.vessel.rootPart) //don't apply EMP buildup per part + { + var EMP = part.vessel.rootPart.FindModuleImplementing(); + if (EMP == null) + { + EMP = (ModuleDrainEC)part.vessel.rootPart.AddModule("ModuleDrainEC"); + } + EMP.incomingDamage = ((EMPRadius / realDistance) * 100); //this way craft at edge of blast might only get disabled instead of bricked + //work on a better EMP damage value, in case of configs with very large thermalRadius + EMP.softEMP = false; //IRL EMP intensity/magnitude enerated by nuke explosion is more or less constant within AoE rather than tapering off, but that's no fun + } + } } else { @@ -518,49 +553,57 @@ private void ExecutePartBlastEvent(PartNukeHitEvent eventToExecute) } else { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.NukeTest]: Applying " + eventToExecute.NegativeForce.ToString("0.0") + " impulse to " + part + " of mass " + part.mass + " at distance " + realDistance + "m"); - part.rb.AddForceAtPosition((Position - part.transform.position).normalized * eventToExecute.NegativeForce * BDArmorySettings.EXP_IMP_MOD * 0.25f, part.transform.position, ForceMode.Impulse); + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log("[BDArmory.NukeFX]: Applying " + eventToExecute.NegativeForce.ToString("0.0") + " impulse to " + part + " of mass " + part.mass + " at distance " + realDistance + "m"); + rb.AddForceAtPosition((Position - part.transform.position).normalized * eventToExecute.NegativeForce * BDArmorySettings.EXP_IMP_MOD * 0.25f, part.transform.position, ForceMode.Impulse); } } } - } + } } // We use an ObjectPool for the ExplosionFx instances as they leak KSPParticleEmitters otherwise. static void SetupPool(string ModelPath, string soundPath, float radius) { - var key = ModelPath + soundPath; - if (!nukePool.ContainsKey(key) || nukePool[key] == null) + if (!string.IsNullOrEmpty(soundPath) && (!audioClips.ContainsKey(soundPath) || audioClips[soundPath] is null)) { - var templateFX = GameDatabase.Instance.GetModel(ModelPath); - if (templateFX == null) + var audioClip = SoundUtils.GetAudioClip(soundPath); + if (audioClip is null) { - Debug.LogError("[BDArmory.NukeFX]: " + ModelPath + " was not found, using the default explosion instead. Please fix your model."); - templateFX = GameDatabase.Instance.GetModel(ModuleWeapon.defaultExplModelPath); + Debug.LogError("[BDArmory.NukeFX]: " + soundPath + " was not found, using the default sound instead. Please fix your model."); + audioClip = SoundUtils.GetAudioClip(ModuleWeapon.defaultExplSoundPath); } - var soundClip = GameDatabase.Instance.GetAudioClip(soundPath); - if (soundClip == null) + audioClips.Add(soundPath, audioClip); + } + + if (!nukePool.ContainsKey(ModelPath) || nukePool[ModelPath] == null) + { + GameObject templateFX; + if (!String.IsNullOrEmpty(ModelPath)) { - Debug.LogError("[BDArmory.NukeFX]: " + soundPath + " was not found, using the default sound instead. Please fix your model."); - soundClip = GameDatabase.Instance.GetAudioClip("BDArmory/Models/explosion/nuke/nukeBoom"); + templateFX = GameDatabase.Instance.GetModel(ModelPath); + if (templateFX == null) + { + //Debug.LogError("[BDArmory.NukeFX]: " + ModelPath + " was not found, using the default explosion instead. Please fix your model."); + templateFX = GameDatabase.Instance.GetModel(ModuleWeapon.defaultExplModelPath); + } } + else templateFX = GameDatabase.Instance.GetModel("BDArmory/Models/shell/model"); //near enough to invisible; model support pre-FXEmitter spawning of Nuke blast FX is only for chernobyl/mutator support for spawning a bomb model in the delay between initializing the nuke and it detonating var eFx = templateFX.AddComponent(); - eFx.ExSound = soundClip; eFx.audioSource = templateFX.AddComponent(); - eFx.audioSource.minDistance = 0; + eFx.audioSource.minDistance = 200; eFx.audioSource.maxDistance = radius * 3; eFx.audioSource.spatialBlend = 1; eFx.audioSource.volume = 5; eFx.LightFx = templateFX.AddComponent(); - eFx.LightFx.color = Utils.ParseColor255("255,238,184,255"); + eFx.LightFx.color = GUIUtils.ParseColor255("255,238,184,255"); eFx.LightFx.intensity = radius / 3; eFx.LightFx.shadows = LightShadows.None; templateFX.SetActive(false); - nukePool[key] = ObjectPool.CreateObjectPool(templateFX, 10, true, true, 0f, false); + nukePool[ModelPath] = ObjectPool.CreateObjectPool(templateFX, 10, true, true, 0f, false); } } public static void CreateExplosion(Vector3 position, ExplosionSourceType explosionSourceType, string sourceVesselName, string sourceWeaponName = "Nuke", - float delay = 2.5f, float blastRadius = 750, float Yield = 0.05f, float thermalShock = 0.05f, bool emp = true, string blastSound = "", + float delay = 2.5f, float blastRadius = 750, float Yield = 0.05f, float thermalShock = 0.05f, bool emp = true, string blastSound = "", string flashModel = "", string shockModel = "", string blastModel = "", string plumeModel = "", string debrisModel = "", string ModelPath = "", string soundPath = "") { SetupPool(ModelPath, soundPath, blastRadius); @@ -585,7 +628,6 @@ public static void CreateExplosion(Vector3 position, ExplosionSourceType explosi eFx.plumeModelPath = plumeModel; eFx.debrisModelPath = debrisModel; eFx.blastSoundPath = blastSound; - Debug.Log("[NUKE FX DEBUG] blast model: " + blastModel); eFx.yield = Yield; eFx.fluence = thermalShock; @@ -593,6 +635,7 @@ public static void CreateExplosion(Vector3 position, ExplosionSourceType explosi eFx.detonationTimer = delay; newExplosion.SetActive(true); eFx.audioSource = newExplosion.GetComponent(); + eFx.SoundPath = soundPath; newExplosion.SetActive(true); } } diff --git a/BDArmory/FX/ParticleTurbulence.cs b/BDArmory/FX/ParticleTurbulence.cs index a4f9bdc58..c6b6a7415 100644 --- a/BDArmory/FX/ParticleTurbulence.cs +++ b/BDArmory/FX/ParticleTurbulence.cs @@ -1,6 +1,7 @@ -using BDArmory.Misc; using UnityEngine; +using BDArmory.Utils; + namespace BDArmory.FX { [KSPAddon(KSPAddon.Startup.Flight, false)] diff --git a/BDArmory/Parts/SeismicChargeFX.cs b/BDArmory/FX/SeismicChargeFX.cs similarity index 94% rename from BDArmory/Parts/SeismicChargeFX.cs rename to BDArmory/FX/SeismicChargeFX.cs index 93e0cf49b..f844439c3 100644 --- a/BDArmory/Parts/SeismicChargeFX.cs +++ b/BDArmory/FX/SeismicChargeFX.cs @@ -1,8 +1,9 @@ using System; -using BDArmory.Core.Extension; +using BDArmory.Extensions; +using BDArmory.Utils; using UnityEngine; -namespace BDArmory.Parts +namespace BDArmory.FX { public class SeismicChargeFX : MonoBehaviour { @@ -30,9 +31,9 @@ void Start() audioSource.maxDistance = 5000; audioSource.dopplerLevel = 0f; audioSource.pitch = UnityEngine.Random.Range(0.93f, 1f); - audioSource.volume = Mathf.Sqrt(originalShipVolume); + audioSource.volume = BDAMath.Sqrt(originalShipVolume); - audioSource.PlayOneShot(GameDatabase.Instance.GetAudioClip("BDArmory/Sounds/seismicCharge")); + audioSource.PlayOneShot(SoundUtils.GetAudioClip("BDArmory/Sounds/seismicCharge")); rb = gameObject.AddComponent(); rb.useGravity = false; diff --git a/BDArmory/FX/ShellCasing.cs b/BDArmory/FX/ShellCasing.cs index 32fc5ab02..0bd154c2b 100644 --- a/BDArmory/FX/ShellCasing.cs +++ b/BDArmory/FX/ShellCasing.cs @@ -1,7 +1,8 @@ -using BDArmory.Core; -using BDArmory.Core.Utils; using UnityEngine; +using BDArmory.Settings; +using BDArmory.Utils; + namespace BDArmory.FX { public class ShellCasing : MonoBehaviour @@ -13,7 +14,7 @@ public class ShellCasing : MonoBehaviour Vector3 angularVelocity; float atmDensity; - int collisionLayerMask = (int)(LayerMasks.Parts | LayerMasks.Scenery | LayerMasks.Unknown19); // Why 19? + const int collisionLayerMask = (int)(LayerMasks.Parts | LayerMasks.Scenery | LayerMasks.Unknown19 | LayerMasks.Wheels); // Why 19? void OnEnable() { @@ -35,8 +36,10 @@ void OnEnable() void FixedUpdate() { - if (!gameObject.activeInHierarchy) + if (!gameObject.activeInHierarchy) return; + if (Time.time - startTime > 2) { + gameObject.SetActive(false); return; } @@ -45,7 +48,7 @@ void FixedUpdate() + Krakensbane.GetLastCorrection(); //drag - velocity -= 0.005f * (velocity + Krakensbane.GetFrameVelocityV3f()) * atmDensity; + velocity -= 0.005f * (velocity + BDKrakensbane.FrameVelocityV3f) * atmDensity; transform.rotation *= Quaternion.Euler(angularVelocity * TimeWarp.fixedDeltaTime); transform.position += velocity * TimeWarp.deltaTime; @@ -62,18 +65,5 @@ void FixedUpdate() } } } - - void Update() - { - if (!gameObject.activeInHierarchy) - { - return; - } - - if (Time.time - startTime > 2) - { - gameObject.SetActive(false); - } - } } } diff --git a/BDArmory/GameModes/Asteroids.cs b/BDArmory/GameModes/Asteroids.cs index 7accaf8dd..48c56863b 100644 --- a/BDArmory/GameModes/Asteroids.cs +++ b/BDArmory/GameModes/Asteroids.cs @@ -3,11 +3,12 @@ using System.Collections.Generic; using System.Linq; using UnityEngine; + using BDArmory.Competition; -using BDArmory.Core; -using BDArmory.Core.Extension; -using BDArmory.Misc; +using BDArmory.Extensions; +using BDArmory.Settings; using BDArmory.UI; +using BDArmory.Utils; namespace BDArmory.GameModes { @@ -244,7 +245,7 @@ public void UpdateSettings(bool warning = false) spawnPoint = FlightGlobals.currentMainBody.GetWorldSurfacePosition(geoCoords.x, geoCoords.y, altitude); } upDirection = (spawnPoint - FlightGlobals.currentMainBody.transform.position).normalized; - spawnPoint += (altitude - Utils.GetRadarAltitudeAtPos(spawnPoint, false)) * upDirection; // Adjust for terrain height. + spawnPoint += (altitude - BodyUtils.GetRadarAltitudeAtPos(spawnPoint, false)) * upDirection; // Adjust for terrain height. refDirection = Math.Abs(Vector3d.Dot(Vector3.up, upDirection)) < 0.71f ? Vector3d.up : Vector3d.forward; // Avoid that the reference direction is colinear with the local surface normal. var a = -(float)FlightGlobals.getGeeForceAtPosition(FlightGlobals.currentMainBody.GetWorldSurfacePosition(geoCoords.x, geoCoords.y, altitude)).magnitude / 2f; @@ -252,7 +253,7 @@ public void UpdateSettings(bool warning = false) var c = altitude; var timeToFall = (-b - Math.Sqrt(b * b - 4f * a * c)) / 2f / a; spawnRate = numberOfAsteroids / timeToFall * Time.fixedDeltaTime; - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log($"[BDArmory.Asteroids]: SpawnRate: {spawnRate} asteroids / frame"); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.Asteroids]: SpawnRate: {spawnRate} asteroids / frame"); if (raining) SetupAsteroidPool(Mathf.RoundToInt(numberOfAsteroids * 1.1f)); // Give ourselves a 10% buffer. } @@ -308,9 +309,9 @@ IEnumerator Rain() { asteroid.Landed = false; asteroid.Splashed = false; - var direction = Vector3.ProjectOnPlane(Quaternion.AngleAxis((float)RNG.NextDouble() * 360f, upDirection) * refDirection, upDirection).normalized; + var direction = (Quaternion.AngleAxis((float)RNG.NextDouble() * 360f, upDirection) * refDirection).ProjectOnPlanePreNormalized(upDirection).normalized; var x = (float)RNG.NextDouble(); - var distance = Mathf.Sqrt(1f - x) * radius; + var distance = BDAMath.Sqrt(1f - x) * radius; StartCoroutine(RepositionWhenReady(asteroid, direction * distance)); } --spawnRateTracker; @@ -343,14 +344,14 @@ void UpdateRainLocation() { foreach (var vessel in LoadedVesselSwitcher.Instance.WeaponManagers.SelectMany(tm => tm.Value).Where(wm => wm != null && wm.vessel != null).Select(wm => wm.vessel)) { maxSqrDistance = Mathf.Max(maxSqrDistance, (vessel.transform.position - averagePosition).sqrMagnitude); } - radius = maxSqrDistance > 5e5f ? Mathf.Sqrt(maxSqrDistance) * 1.5f : 1000f; + radius = maxSqrDistance > 5e5f ? BDAMath.Sqrt(maxSqrDistance) * 1.5f : 1000f; } } else { if (Vector3d.Dot(upDirection, (FlightGlobals.currentMainBody.GetWorldSurfacePosition(geoCoords.x, geoCoords.y, altitude) - FlightGlobals.currentMainBody.transform.position).normalized) < 0.99) // Planet rotation has moved the spawn point and direction significantly. { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.Asteroids]: Planet has rotated significantly, updating settings."); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log("[BDArmory.Asteroids]: Planet has rotated significantly, updating settings."); } } UpdateSettings(); @@ -371,12 +372,12 @@ IEnumerator RepositionWhenReady(Vessel asteroid, Vector3 offset) { spawnPoint = FlightGlobals.currentMainBody.GetWorldSurfacePosition(geoCoords.x, geoCoords.y, altitude); var position = spawnPoint + offset; - position += (altitude - Utils.GetRadarAltitudeAtPos(position, false)) * upDirection; + position += (altitude - BodyUtils.GetRadarAltitudeAtPos(position, false)) * upDirection; asteroid.transform.position = position; asteroid.SetWorldVelocity(initialSpeed * upDirection); // Apply a gaussian random torque to the asteroid. asteroid.rootPart.Rigidbody.angularVelocity = Vector3.zero; - asteroid.rootPart.Rigidbody.AddTorque(Misc.VectorUtils.GaussianVector3d(Vector3d.zero, 300 * Vector3d.one), ForceMode.Acceleration); + asteroid.rootPart.Rigidbody.AddTorque(VectorUtils.GaussianVector3d(Vector3d.zero, 300 * Vector3d.one), ForceMode.Acceleration); } } @@ -425,7 +426,7 @@ IEnumerator RemoveAfterDelay(Vessel asteroid, float delay) beingRemoved.Remove(asteroid); } else - { if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log($"[BDArmory.Asteroids]: Asteroid {asteroid.vesselName} is null, unable to remove."); } + { if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.Asteroids]: Asteroid {asteroid.vesselName} is null, unable to remove."); } } #endregion @@ -472,7 +473,7 @@ void SetupAsteroidPool(int count) /// void ReplacePooledAsteroid(int i) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log($"[BDArmory.Asteroids]: Replacing asteroid at position {i}."); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.Asteroids]: Replacing asteroid at position {i}."); var asteroid = AsteroidUtils.SpawnAsteroid(FlightGlobals.currentMainBody.GetWorldSurfacePosition(geoCoords.x, geoCoords.y, altitude + 10000)); if (asteroid != null) { @@ -493,7 +494,7 @@ void AddAsteroidsToPool(int count) var refDirection = Math.Abs(Vector3d.Dot(Vector3.up, upDirection)) < 0.71f ? Vector3d.up : Vector3d.forward; // Avoid that the reference direction is colinear with the local surface normal. for (int i = 0; i < count; ++i) { - var direction = Vector3.ProjectOnPlane(Quaternion.AngleAxis(i / 60f * 360f, upDirection) * refDirection, upDirection).normalized; // 60 asteroids per layer of the spiral (approx. 100m apart). + var direction = (Quaternion.AngleAxis(i / 60f * 360f, upDirection) * refDirection).ProjectOnPlanePreNormalized(upDirection).normalized; // 60 asteroids per layer of the spiral (approx. 100m apart). var position = spawnPoint + (1e4f + 1e2f * i / 60) * upDirection + 1e3f * direction; // 100m altitude difference per layer of the spiral. var asteroid = AsteroidUtils.SpawnAsteroid(position); if (asteroid != null) @@ -712,12 +713,12 @@ IEnumerator SpawnField(int numberOfAsteroids) asteroids = new Vessel[numberOfAsteroids]; for (int i = 0; i < asteroids.Length; ++i) { - var direction = Vector3.ProjectOnPlane(Quaternion.AngleAxis((float)RNG.NextDouble() * 360f, upDirection) * refDirection, upDirection).normalized; + var direction = (Quaternion.AngleAxis((float)RNG.NextDouble() * 360f, upDirection) * refDirection).ProjectOnPlanePreNormalized(upDirection).normalized; var x = (float)RNG.NextDouble(); - var distance = Mathf.Sqrt(1f - x) * radius; + var distance = BDAMath.Sqrt(1f - x) * radius; var height = RNG.NextDouble() * (altitude - 50f) + 50f; var position = spawnPoint + direction * distance; - position += (height - Utils.GetRadarAltitudeAtPos(position)) * upDirection; + position += (height - BodyUtils.GetRadarAltitudeAtPos(position)) * upDirection; var asteroid = GetAsteroid(); if (asteroid != null) { @@ -775,7 +776,7 @@ IEnumerator SetInitialRotation(Vessel asteroid) if (asteroid != null && asteroid.gameObject.activeInHierarchy) { asteroid.rootPart.Rigidbody.angularVelocity = Vector3.zero; - asteroid.rootPart.Rigidbody.AddTorque(Misc.VectorUtils.GaussianVector3d(Vector3d.zero, 50 * Vector3d.one), ForceMode.Acceleration); // Apply a gaussian random torque to each asteroid. + asteroid.rootPart.Rigidbody.AddTorque(VectorUtils.GaussianVector3d(Vector3d.zero, 50 * Vector3d.one), ForceMode.Acceleration); // Apply a gaussian random torque to each asteroid. } } @@ -821,7 +822,7 @@ void SetupAsteroidPool(int count) /// void ReplacePooledAsteroid(int i) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log($"[BDArmory.Asteroids]: Replacing asteroid at position {i}."); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.Asteroids]: Replacing asteroid at position {i}."); var asteroid = AsteroidUtils.SpawnAsteroid(FlightGlobals.currentMainBody.GetWorldSurfacePosition(geoCoords.x, geoCoords.y, altitude + 10000)); if (asteroid != null) { @@ -842,7 +843,7 @@ void AddAsteroidsToPool(int count) var refDirection = Math.Abs(Vector3d.Dot(Vector3.up, upDirection)) < 0.71f ? Vector3d.up : Vector3d.forward; // Avoid that the reference direction is colinear with the local surface normal. for (int i = 0; i < count; ++i) { - var direction = Vector3.ProjectOnPlane(Quaternion.AngleAxis(i / 60f * 360f, upDirection) * refDirection, upDirection).normalized; // 60 asteroids per layer of the spiral (approx. 100m apart). + var direction = (Quaternion.AngleAxis(i / 60f * 360f, upDirection) * refDirection).ProjectOnPlanePreNormalized(upDirection).normalized; // 60 asteroids per layer of the spiral (approx. 100m apart). var position = spawnPoint + (1e4f + 1e2f * i / 60) * upDirection + 1e3f * direction; // 100m altitude difference per layer of the spiral. var asteroid = AsteroidUtils.SpawnAsteroid(position); if (asteroid != null) diff --git a/BDArmory/GameModes/BattleDamageHandler.cs b/BDArmory/GameModes/BattleDamage/BattleDamageHandler.cs similarity index 91% rename from BDArmory/GameModes/BattleDamageHandler.cs rename to BDArmory/GameModes/BattleDamage/BattleDamageHandler.cs index 95228d276..732b310e0 100644 --- a/BDArmory/GameModes/BattleDamageHandler.cs +++ b/BDArmory/GameModes/BattleDamage/BattleDamageHandler.cs @@ -2,13 +2,19 @@ using System.Linq; using UnityEngine; +using BDArmory.Ammo; using BDArmory.Competition; -using BDArmory.Core.Extension; -using BDArmory.Core.Module; -using BDArmory.Core; +using BDArmory.Control; +using BDArmory.CounterMeasure; +using BDArmory.Damage; +using BDArmory.Extensions; using BDArmory.FX; -using BDArmory.Misc; using BDArmory.Modules; +using BDArmory.Radar; +using BDArmory.Settings; +using BDArmory.Targeting; +using BDArmory.Utils; +using BDArmory.WeaponMounts; namespace BDArmory.GameModes { @@ -74,7 +80,7 @@ public static void CheckDamageFX(Part part, float caliber, float penetrationFact { Diceroll *= 0.66; } - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BattleDamageHandler]: Battery Dice Roll: " + Diceroll); + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log("[BDArmory.BattleDamageHandler]: Battery Dice Roll: " + Diceroll); if (Diceroll <= BDArmorySettings.BD_DAMAGE_CHANCE) { BulletHitFX.AttachFire(hitLoc.point, part, caliber, attacker); @@ -85,12 +91,12 @@ public static void CheckDamageFX(Part part, float caliber, float penetrationFact var Armor = part.FindModuleImplementing(); if (Armor != null) { - if (Armor.HullTypeNum == 1) //wooden parts can potentially catch fire + if (Armor.ignitionTemp > 0) //wooden parts can potentially catch fire { if (incendiary) { double Diceroll = UnityEngine.Random.Range(0, 100); - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BattleDamageHandler]: Wood part Dice Roll: " + Diceroll); + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log("[BDArmory.BattleDamageHandler]: Wood part Dice Roll: " + Diceroll); if (Diceroll <= BDArmorySettings.BD_DAMAGE_CHANCE) { BulletHitFX.AttachFire(hitLoc.point, part, caliber, attacker, 90); @@ -112,7 +118,7 @@ public static void CheckDamageFX(Part part, float caliber, float penetrationFact { Diceroll *= 0.66; } - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BattleDamageHandler]: Ammo TAC DiceRoll: " + Diceroll + "; needs: " + damageChance); + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log("[BDArmory.BattleDamageHandler]: Ammo TAC DiceRoll: " + Diceroll + "; needs: " + damageChance); if (Diceroll <= (damageChance) && part.GetDamagePercentage() < 0.95f) { ammo.DetonateIfPossible(); @@ -139,7 +145,7 @@ public static void CheckDamageFX(Part part, float caliber, float penetrationFact engine.thrustPercentage -= ((((tracker.oldDamagePercent - part.GetDamagePercentage())) * (penetrationFactor / 2)) * BDArmorySettings.BD_PROP_DAM_RATE) * 10; //convert from damagepercent to thrustpercent //use difference in old Hp and current, not just current, else it doesn't matter if its a heavy hit or chipped paint, thrust reduction is the same engine.thrustPercentage = Mathf.Clamp(engine.thrustPercentage, BDArmorySettings.BD_PROP_FLOOR, 100); //even heavily damaged engines will still put out something - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BattleDamageHandler]: engine thrust: " + engine.thrustPercentage); + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log("[BDArmory.BattleDamageHandler]: engine thrust: " + engine.thrustPercentage); engine.PlayFlameoutFX(true); tracker.oldDamagePercent = part.GetDamagePercentage(); /* @@ -238,7 +244,7 @@ public static void CheckDamageFX(Part part, float caliber, float penetrationFact intake.area -= (tracker.origIntakeArea * (((tracker.oldDamagePercent - part.GetDamagePercentage()) * HEBonus) * BDArmorySettings.BD_PROP_DAM_RATE)); //HE does bonus damage intake.area = Mathf.Clamp((float)intake.area, ((float)tracker.origIntakeArea / 4), 99999); //even shredded intake ducting will still get some air to engines - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BattleDamageHandler]: Intake damage: Orig Area: " + tracker.origIntakeArea + "; Current Area: " + intake.area + "; Intake Speed: " + intake.intakeSpeed + "; intake damage: " + (1 - ((((tracker.oldDamagePercent - part.GetDamagePercentage())) * HEBonus) / BDArmorySettings.BD_PROP_DAM_RATE))); + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log("[BDArmory.BattleDamageHandler]: Intake damage: Orig Area: " + tracker.origIntakeArea + "; Current Area: " + intake.area + "; Intake Speed: " + intake.intakeSpeed + "; intake damage: " + (1 - ((((tracker.oldDamagePercent - part.GetDamagePercentage())) * HEBonus) / BDArmorySettings.BD_PROP_DAM_RATE))); } } if (BDArmorySettings.BD_GIMBALS) //engine gimbal damage @@ -257,7 +263,7 @@ public static void CheckDamageFX(Part part, float caliber, float penetrationFact } //gimbal.gimbalRange *= (1 - (((1 - part.GetDamagePercentatge()) * HEBonus) / BDArmorySettings.BD_PROP_DAM_RATE)); //HE does bonus damage double Diceroll = UnityEngine.Random.Range(0, 100); - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BattleDamageHandler]: Gimbal DiceRoll: " + Diceroll); + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log("[BDArmory.BattleDamageHandler]: Gimbal DiceRoll: " + Diceroll); if (Diceroll <= (BDArmorySettings.BD_DAMAGE_CHANCE * HEBonus)) { gimbal.enabled = false; @@ -287,8 +293,8 @@ public static void CheckDamageFX(Part part, float caliber, float penetrationFact //2x4m wing board = 2 Lift, 0.25 Lift/m2. 20mm round = 20*20=400/20000= 0.02 Lift reduced per hit, 100 rounds to reduce lift to 0. mind you, it only takes ~15 rounds to destroy the wing... if (wing.deflectionLiftCoeff > ((part.mass * 5) + liftDam)) //stock mass/lift ratio is 10; 0.2t wing has 2.0 lift; clamp lift lost at half { - wing.deflectionLiftCoeff -= liftDam; - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BattleDamageHandler]: " + part.name + "hit by " + caliber + " round, penFactor " + penetrationFactor + "; took lift damage: " + liftDam + ", current lift: " + wing.deflectionLiftCoeff); + wing.deflectionLiftCoeff -= liftDam; + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log("[BDArmory.BattleDamageHandler]: " + part.name + "hit by " + caliber + " round, penFactor " + penetrationFactor + "; took lift damage: " + liftDam + ", current lift: " + wing.deflectionLiftCoeff); } } if (BDArmorySettings.BD_CTRL_SRF && firsthit) @@ -337,7 +343,7 @@ public static void CheckDamageFX(Part part, float caliber, float penetrationFact if (BDArmorySettings.BD_SUBSYSTEMS && firsthit) { double Diceroll = UnityEngine.Random.Range(0, 100); - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BattleDamageHandler]: Subsystem DiceRoll: " + Diceroll + "; needs: " + damageChance); + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log("[BDArmory.BattleDamageHandler]: Subsystem DiceRoll: " + Diceroll + "; needs: " + damageChance); if (Diceroll <= (damageChance) && part.GetDamagePercentage() < 0.95f) { if (part.GetComponent() != null) //should have this be separate dice rolls, else a part with more than one of these will lose them all @@ -400,7 +406,7 @@ public static void CheckDamageFX(Part part, float caliber, float penetrationFact cam = part.GetComponent(); // gimbal range?? part.RemoveModule(cam); } - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BattleDamageHandler]: " + part.name + "took subsystem damage"); + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log("[BDArmory.BattleDamageHandler]: " + part.name + "took subsystem damage"); if (Diceroll <= (damageChance / 2)) { if (incendiary) @@ -416,7 +422,7 @@ public static void CheckDamageFX(Part part, float caliber, float penetrationFact if (part.GetComponent() != null) { double ControlDiceRoll = UnityEngine.Random.Range(0, 100); - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BattleDamageHandler]: Command DiceRoll: " + ControlDiceRoll); + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log("[BDArmory.BattleDamageHandler]: Command DiceRoll: " + ControlDiceRoll); if (ControlDiceRoll <= (BDArmorySettings.BD_DAMAGE_CHANCE * 2)) { using (List.Enumerator craftPart = part.vessel.parts.GetEnumerator()) @@ -445,7 +451,7 @@ public static void CheckDamageFX(Part part, float caliber, float penetrationFact } } //GuardRange reduction to sim canopy/sensor damage? - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BattleDamageHandler]: " + part.name + "took command damage"); + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log("[BDArmory.BattleDamageHandler]: " + part.name + "took command damage"); } } } @@ -465,7 +471,7 @@ public static void CheckDamageFX(Part part, float caliber, float penetrationFact { float PilotTAC = Mathf.Clamp((BDArmorySettings.BD_DAMAGE_CHANCE / part.mass), 0.01f, 100); //larger cockpits = greater volume = less chance any hit will pass through a region of volume containing a pilot float killchance = UnityEngine.Random.Range(0, 100); - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BattleDamageHandler]: Pilot TAC: " + PilotTAC + "; dice roll: " + killchance); + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log("[BDArmory.BattleDamageHandler]: Pilot TAC: " + PilotTAC + "; dice roll: " + killchance); if (killchance <= PilotTAC) //add penetrationfactor threshold? hp threshold? { ProtoCrewMember crewMember = part.protoModuleCrew.FirstOrDefault(x => x != null); @@ -489,7 +495,7 @@ public static void CheckDamageFX(Part part, float caliber, float penetrationFact crewMember.StartRespawnPeriod(); } //ScreenMessages.PostScreenMessage(crewMember.name + " killed by damage to " + part.vessel.name + part.partName + ".", 5.0f, ScreenMessageStyle.UPPER_LEFT); - ScreenMessages.PostScreenMessage("Cockpit snipe on " + part.vessel.GetName() + "! " + crewMember.name + " killed!", 5.0f, ScreenMessageStyle.UPPER_LEFT); + ScreenMessages.PostScreenMessage("Cockpit snipe on " + part.vessel.GetName() + "! " + crewMember.name + " killed!", 5.0f, ScreenMessageStyle.UPPER_CENTER); BDACompetitionMode.Instance.OnVesselModified(part.vessel); } diff --git a/BDArmory/GameModes/BattleDamageTracker.cs b/BDArmory/GameModes/BattleDamage/BattleDamageTracker.cs similarity index 100% rename from BDArmory/GameModes/BattleDamageTracker.cs rename to BDArmory/GameModes/BattleDamage/BattleDamageTracker.cs diff --git a/BDArmory/GameModes/ModuleSpaceFriction.cs b/BDArmory/GameModes/ModuleSpaceFriction.cs new file mode 100644 index 000000000..145158c1a --- /dev/null +++ b/BDArmory/GameModes/ModuleSpaceFriction.cs @@ -0,0 +1,254 @@ +using System.Linq; +using UnityEngine; + +using BDArmory.Control; +using BDArmory.Settings; +using BDArmory.Utils; +using System.Collections.Generic; + +namespace BDArmory.GameModes +{ + public class ModuleSpaceFriction : PartModule + { + /// + /// Adds friction/drag to craft in null-atmo porportional to AI MaxSpeed setting to ensure craft does not exceed said speed + /// Adds counter-gravity to prevent null-atmo ships from falling to the ground from gravity in the absence of wings and lift + /// Provides additional friction/drag during corners to help spacecraft drift through turns instead of being stuck with straight-up joust charges + /// TL;DR, provides the means for SciFi style space dogfights + /// + + private double frictionCoeff = 1.0f; //how much force is applied to decellerate craft + + //[KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "Space Friction"), UI_Toggle(disabledText = "Disabled", enabledText = "Enabled", scene = UI_Scene.All, affectSymCounterparts = UI_Scene.All)] + //public bool FrictionEnabled = false; //global value + + //[KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "CounterGrav"), UI_Toggle(disabledText = "Disabled", enabledText = "Enabled", scene = UI_Scene.All, affectSymCounterparts = UI_Scene.All)] + //public bool AntiGravEnabled = false; //global value + + [KSPField(isPersistant = true)] + public bool AntiGravOverride = false; //per craft override to be set in the .craft file, for things like zeppelin battles where attacking planes shouldn't be under countergrav + [KSPField(isPersistant = true)] + public bool RepulsorOverride = false; + public float maxVelocity = 300; //MaxSpeed setting in PilotAI + + public float frictMult; //engine thrust of craft + + float targetAlt = 25; + //public float driftMult = 2; //additional drag multipler for cornering/decellerating so things don't take the same amount of time to decelerate as they do to accelerate + + List repulsors; + List spaceFrictionModules; + public static bool GameIsPaused + { + get { return PauseMenu.isOpen || Time.timeScale == 0; } + } + BDModulePilotAI AI; + public BDModulePilotAI pilot + { + get + { + if (AI) return AI; + AI = VesselModuleRegistry.GetBDModulePilotAI(vessel, true); // FIXME should this be IBDAIControl? + return AI; + } + } + BDModuleSurfaceAI SAI; + public BDModuleSurfaceAI driver + { + get + { + if (SAI) return SAI; + SAI = VesselModuleRegistry.GetBDModuleSurfaceAI(vessel, true); + return SAI; + } + } + + BDModuleVTOLAI VAI; + public BDModuleVTOLAI flier + { + get + { + if (VAI) return VAI; + VAI = VesselModuleRegistry.GetModule(vessel); + + return VAI; + } + } + ModuleEngines Engine; + public ModuleEngines foundEngine + { + get + { + if (Engine) return Engine; + Engine = VesselModuleRegistry.GetModuleEngines(vessel).FirstOrDefault(); + return Engine; + } + } + MissileFire MF; + public MissileFire weaponManager + { + get + { + if (MF) return MF; + MF = VesselModuleRegistry.GetMissileFire(vessel, true); + return MF; + } + } + + void Start() + { + if (vessel.rootPart == this.part) //if we're an external non-root repulsor part, don't check for dupes in root. + { + foreach (var repMod in vessel.rootPart.FindModulesImplementing()) + { + if (repMod != this) + { + // Not really sure how this is happening, but it is. It looks a bit like a race condition somewhere is allowing this module to be added twice. + Debug.LogWarning($"[BDArmory.GameModes.ModuleSpaceFriction]: Found a duplicate space friction module on root part of {vessel.vesselName}! Removing..."); + Destroy(repMod); + } + } + } + if (HighLogic.LoadedSceneIsFlight) + { + if (!RepulsorOverride) //MSF added via Spawn utilities for Space Hacks + { + using (var engine = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) + while (engine.MoveNext()) + { + if (engine.Current == null) continue; + if (engine.Current.independentThrottle) continue; //only grab primary thrust engines + frictMult += (engine.Current.maxThrust * (engine.Current.thrustPercentage / 100)); //FIXME - Look into grabbing max thrust from velCurve, if for whatever reason a rocket engine has one of these + //have this called onvesselModified? + } + frictMult /= 6; //doesn't need to be 100% of thrust at max speed, Ai will already self-limit; this also has the AI throttle down, which allows for slamming the throttle full for braking/coming about, instead of being stuck with lower TwR + repulsors = VesselModuleRegistry.GetRepulsorModules(vessel); + using (var r = repulsors.GetEnumerator()) + while (r.MoveNext()) + { + if (r.Current == null) continue; + r.Current.part.PhysicsSignificance = 1; //only grab primary thrust engines + } + } + else + { + spaceFrictionModules = VesselModuleRegistry.GetModules(vessel); + } + } + } + + public void FixedUpdate() + { + if ((!BDArmorySettings.SPACE_HACKS && (!AntiGravOverride && !RepulsorOverride)) || !HighLogic.LoadedSceneIsFlight || !FlightGlobals.ready || this.vessel.packed || GameIsPaused) return; + + if (this.part.vessel.situation == Vessel.Situations.FLYING || this.part.vessel.situation == Vessel.Situations.SUB_ORBITAL) + { + if (BDArmorySettings.SF_FRICTION) + { + if (this.part.vessel.speed > 10) + { + if (AI != null) + { + maxVelocity = AI.maxSpeed; + } + else if (SAI != null) + { + maxVelocity = SAI.MaxSpeed; + } + else if (VAI != null) + maxVelocity = VAI.MaxSpeed; + + var speedFraction = (float)part.vessel.speed / maxVelocity; + if (speedFraction > 1) speedFraction = Mathf.Max(2, speedFraction); + frictionCoeff = speedFraction * speedFraction * speedFraction * frictMult; //at maxSpeed, have friction be 100% of vessel's engines thrust + + frictionCoeff *= (1 + (Vector3.Angle(this.part.vessel.srf_vel_direction, this.part.vessel.GetTransform().up) / 180) * BDArmorySettings.SF_DRAGMULT * 4); //greater AoA off prograde, greater drag + frictionCoeff /= vessel.Parts.Count; + //part.vessel.rootPart.rb.AddForceAtPosition((-part.vessel.srf_vel_direction * frictionCoeff), part.vessel.CoM, ForceMode.Acceleration); + for (int i = 0; i < part.vessel.Parts.Count; i++) + { + if (part.vessel.parts[i].PhysicsSignificance != 1) //attempting to apply rigidbody force to non-significant parts will NRE + { + part.vessel.Parts[i].Rigidbody.AddForceAtPosition((-part.vessel.srf_vel_direction * frictionCoeff), part.vessel.CoM, ForceMode.Acceleration); + } + } + } + } + if (BDArmorySettings.SF_GRAVITY || AntiGravOverride) //have this disabled if no engines left? + { + if (weaponManager != null && foundEngine != null) //have engineless craft fall + { + for (int i = 0; i < part.vessel.Parts.Count; i++) + { + if (part.vessel.parts[i].PhysicsSignificance != 1) //attempting to apply rigidbody force to non-significant parts will NRE + { + part.vessel.Parts[i].Rigidbody.AddForce(-FlightGlobals.getGeeForceAtPosition(part.vessel.Parts[i].transform.position), ForceMode.Acceleration); + } + } + } + } + } + if (this.part.vessel.situation != Vessel.Situations.ORBITING || this.part.vessel.situation != Vessel.Situations.DOCKED || this.part.vessel.situation != Vessel.Situations.ESCAPING || this.part.vessel.situation != Vessel.Situations.PRELAUNCH) + { + if (BDArmorySettings.SF_REPULSOR || RepulsorOverride) + { + if ((pilot != null || driver != null || flier != null || RepulsorOverride) && foundEngine != null) + { + targetAlt = 10; + if (AI != null) + { + targetAlt = AI.defaultAltitude; // Use default alt instead of min alt to keep the vessel away from 'gain alt' behaviour. + } + else if (SAI != null) + { + targetAlt = SAI.MaxSlopeAngle * 2; + } + else if (VAI != null) + targetAlt = VAI.defaultAltitude; + + Vector3d grav = FlightGlobals.getGeeForceAtPosition(vessel.CoM); + var vesselMass = part.vessel.GetTotalMass(); + if (RepulsorOverride) //Asking this first, so SPACEHACKS repulsor mode will ignore it + { + float pointAltitude = BodyUtils.GetRadarAltitudeAtPos(part.transform.position); + if (pointAltitude <= 0 || pointAltitude > 2f * targetAlt) return; + var factor = Mathf.Clamp(Mathf.Exp(BDArmorySettings.SF_REPULSOR_STRENGTH * (targetAlt - pointAltitude) / targetAlt - (float)vessel.verticalSpeed / targetAlt), 0f, 5f * BDArmorySettings.SF_REPULSOR_STRENGTH); // Decaying exponential balanced at the target altitude with velocity damping. + float repulsorForce = vesselMass * factor / spaceFrictionModules.Count; // Spread the force between the repulsors. + if (float.IsNaN(factor) || float.IsInfinity(factor)) // This should only happen if targetAlt is 0, which should never happen. + Debug.LogWarning($"[BDArmory.Spacehacks]: Repulsor Force is NaN or Infinity. TargetAlt: {targetAlt}, point Alt: {pointAltitude}, VesselMass: {vesselMass}"); + else + part.Rigidbody.AddForce(-grav * repulsorForce, ForceMode.Force); + } + else + { + using (var repulsor = repulsors.GetEnumerator()) + while (repulsor.MoveNext()) + { + if (repulsor.Current == null) continue; + float pointAltitude = BodyUtils.GetRadarAltitudeAtPos(repulsor.Current.transform.position); + if (pointAltitude <= 0 || pointAltitude > 2f * targetAlt) continue; + var factor = Mathf.Clamp(Mathf.Exp(BDArmorySettings.SF_REPULSOR_STRENGTH * (targetAlt - pointAltitude) / targetAlt - (float)vessel.verticalSpeed / targetAlt), 0f, 5f * BDArmorySettings.SF_REPULSOR_STRENGTH); // Decaying exponential balanced at the target altitude with velocity damping. + float repulsorForce = vesselMass * factor / repulsors.Count; // Spread the force between the repulsors. + if (float.IsNaN(factor) || float.IsInfinity(factor)) // This should only happen if targetAlt is 0, which should never happen. + Debug.LogWarning($"[BDArmory.Spacehacks]: Repulsor Force is NaN or Infinity. TargetAlt: {targetAlt}, point Alt: {pointAltitude}, VesselMass: {vesselMass}"); + else + repulsor.Current.part.Rigidbody.AddForce(-grav * repulsorForce, ForceMode.Force); + } + } + } + } + } + } + + public static void AddSpaceFrictionToAllValidVessels() + { + foreach (var vessel in FlightGlobals.Vessels) + { + if (VesselModuleRegistry.GetMissileFire(vessel, true) != null && vessel.rootPart.FindModuleImplementing() == null) + { + vessel.rootPart.AddModule("ModuleSpaceFriction"); + } + } + } + } +} diff --git a/BDArmory/Modules/BDAMutator.cs b/BDArmory/GameModes/Mutators/BDAMutator.cs similarity index 91% rename from BDArmory/Modules/BDAMutator.cs rename to BDArmory/GameModes/Mutators/BDAMutator.cs index 812fbece8..1ccf8f083 100644 --- a/BDArmory/Modules/BDAMutator.cs +++ b/BDArmory/GameModes/Mutators/BDAMutator.cs @@ -1,17 +1,18 @@ -using BDArmory.Core; -using BDArmory.Competition; -using BDArmory.GameModes; -using BDArmory.Misc; -using System.Collections.Generic; -using UnityEngine; -using BDArmory.Core.Extension; -using BDArmory.Core.Module; -using BDArmory.UI; -using BDArmory.FX; +using System.Collections.Generic; using System.Linq; +using UnityEngine; + using BDArmory.Bullets; +using BDArmory.Competition; +using BDArmory.Damage; +using BDArmory.Extensions; +using BDArmory.FX; +using BDArmory.Settings; +using BDArmory.UI; +using BDArmory.Utils; +using BDArmory.Weapons; -namespace BDArmory.Modules +namespace BDArmory.GameModes { class BDAMutator : PartModule { @@ -62,7 +63,7 @@ public override void OnStart(StartState state) var indices = Enumerable.Range(0, BDArmorySettings.MUTATOR_LIST.Count).ToList(); indices.Shuffle(); name = string.Join("; ", indices.Take(BDArmorySettings.MUTATOR_APPLY_NUM).Select(i => MutatorInfo.mutators[BDArmorySettings.MUTATOR_LIST[i]].name)); - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDAMutator] random mutator list built: " + name + " on " + part.vessel.GetName()); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log("[BDArmory.BDAMutator]: random mutator list built: " + name + " on " + part.vessel.GetName()); } mutatorName = name; mutators = BDAcTools.ParseNames(name); @@ -70,7 +71,7 @@ public override void OnStart(StartState state) { name = MutatorInfo.mutators[mutators[r]].name; mutatorInfo = MutatorInfo.mutators[name]; - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDAMutator] beginning mutator initialization of " + name + " on " + part.vessel.GetName()); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log("[BDArmory.BDAMutator]: beginning mutator initialization of " + name + " on " + part.vessel.GetName()); if (mutatorInfo.weaponMod) { @@ -122,9 +123,12 @@ public override void OnStart(StartState state) else { var WM = VesselModuleRegistry.GetMissileFire(part.vessel, true); - string color = $"{Mathf.RoundToInt(BDTISetup.Instance.ColorAssignments[WM.Team.Name].r * 255)},{Mathf.RoundToInt(BDTISetup.Instance.ColorAssignments[WM.Team.Name].g * 255)},{Mathf.RoundToInt(BDTISetup.Instance.ColorAssignments[WM.Team.Name].b * 255)},{Mathf.RoundToInt(BDTISetup.Instance.ColorAssignments[WM.Team.Name].a * 255)}"; + string color = $"{Mathf.RoundToInt(BDTISetup.Instance.ColorAssignments[WM.Team.Name].r * 255)},{Mathf.RoundToInt(BDTISetup.Instance.ColorAssignments[WM.Team.Name].g * 255)},{Mathf.RoundToInt(BDTISetup.Instance.ColorAssignments[WM.Team.Name].b * 255)},{Mathf.RoundToInt(BDTISetup.Instance.ColorAssignments[WM.Team.Name].a * 255)}"; weapon.Current.projectileColor = color; + } + Debug.Log($"[MUTATOR BEAM DEBUG] Beam color for {part} on {vessel.GetName()} set to {weapon.Current.projectileColor}"); + weapon.Current.laserTexList = BDAcTools.ParseNames(weapon.Current.laserTexturePath); weapon.Current.SetupLaserSpecifics(); weapon.Current.pulseLaser = true; } @@ -134,8 +138,6 @@ public override void OnStart(StartState state) weapon.Current.externalAmmo = true; } weapon.Current.resourceSteal = mutatorInfo.resourceSteal; - weapon.Current.impulseWeapon = false; - weapon.Current.graviticWeapon = false; //Debug.Log("[MUTATOR] current weapon status: " + weapon.Current.WeaponStatusdebug()); } } @@ -215,7 +217,7 @@ public void DisableMutator() { if (!mutatorEnabled) return; mutatorEnabled = false; - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDAMutator]: Disabling " + mutatorInfo.name + "Mutator on " + part.vessel.vesselName); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log("[BDArmory.BDAMutator]: Disabling " + mutatorInfo.name + "Mutator on " + part.vessel.vesselName); using (var weapon = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) while (weapon.MoveNext()) { @@ -229,8 +231,6 @@ public void DisableMutator() weapon.Current.maxDeviation = weapon.Current.baseDeviation; weapon.Current.laserDamage = weapon.Current.baseLaserdamage; weapon.Current.pulseLaser = weapon.Current.pulseInConfig; - weapon.Current.impulseWeapon = weapon.Current.ImpulseInConfig; - weapon.Current.graviticWeapon = weapon.Current.GraviticInConfig; weapon.Current.instagib = false; weapon.Current.strengthMutator = 1; weapon.Current.SetupAmmo(null, null); @@ -265,7 +265,7 @@ public void DisableMutator() hasTaxes = false; } - void Update() + void FixedUpdate() { if (HighLogic.LoadedSceneIsFlight && !BDArmorySetup.GameIsPaused && !vessel.packed) { @@ -321,7 +321,7 @@ void Update() } catch { - Debug.Log("[BDAMutator] mutator not configured correctly. Set ResourceTaxRate to 0 or add resource to ResourceTax"); + Debug.Log("[BDArmory.BDAMutator]: mutator not configured correctly. Set ResourceTaxRate to 0 or add resource to ResourceTax"); } } } @@ -348,7 +348,7 @@ void OnGUI() { if (mutatorEnabled) { - Vector3 screenPos = BDGUIUtils.GetMainCamera().WorldToViewportPoint(vessel.CoM); + Vector3 screenPos = GUIUtils.GetMainCamera().WorldToViewportPoint(vessel.CoM); if (screenPos.z < 0) return; //dont draw if point is behind camera if (screenPos.x != Mathf.Clamp01(screenPos.x)) return; //dont draw if off screen if (screenPos.y != Mathf.Clamp01(screenPos.y)) return; @@ -361,7 +361,7 @@ void OnGUI() iconPath = MutatorInfo.mutators[mutators[i]].icon; iconcolor = MutatorInfo.mutators[mutators[i]].iconColor; - iconColor = Utils.ParseColor255(iconcolor); + iconColor = GUIUtils.ParseColor255(iconcolor); switch (iconPath) { case "IconAccuracy": @@ -427,8 +427,8 @@ void Detonate() { if (!Vengeance) return; if (!BDACompetitionMode.Instance.competitionIsActive) return; - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDAMutator] triggering vengeance nuke"); - NukeFX.CreateExplosion(part.transform.position, ExplosionSourceType.BattleDamage, this.vessel.GetName(), "Vengeance Explosion", 2.5f, 300, 1.5f, 1.5f, true, + if (BDArmorySettings.DEBUG_OTHER) Debug.Log("[BDArmory.BDAMutator]: triggering vengeance nuke"); + NukeFX.CreateExplosion(part.transform.position, ExplosionSourceType.Other, this.vessel.GetName(), "Vengeance Explosion", 2.5f, 300, 1.5f, 1.5f, true, "BDArmory/Models/explosion/nuke/nukeBoom", "BDArmory/Models/explosion/nuke/nukeFlash", "BDArmory/Models/explosion/nuke/nukeShock", "BDArmory/Models/explosion/nuke/nukeBlast", "BDArmory/Models/explosion/nuke/nukePlume", "BDArmory/Models/explosion/nuke/nukeScatter", "BDArmory/Models/Mutators/Vengence", ""); } diff --git a/BDArmory/GameModes/MutatorInfo.cs b/BDArmory/GameModes/Mutators/MutatorInfo.cs similarity index 100% rename from BDArmory/GameModes/MutatorInfo.cs rename to BDArmory/GameModes/Mutators/MutatorInfo.cs diff --git a/BDArmory/GameModes/Waypoints/WaypointCourses.cs b/BDArmory/GameModes/Waypoints/WaypointCourses.cs new file mode 100644 index 000000000..caedda8c4 --- /dev/null +++ b/BDArmory/GameModes/Waypoints/WaypointCourses.cs @@ -0,0 +1,271 @@ +using System; +using System.IO; +using System.Collections.Generic; +using UniLinq; +using UnityEngine; +using BDArmory.Settings; + +namespace BDArmory.GameModes.Waypoints +{ + [KSPAddon(KSPAddon.Startup.Flight, false)] + public class WaypointCourses : MonoBehaviour + { + private static WaypointCourses Instance; + + // Interesting spawn locations on Kerbin. + [WaypointField] public static bool UpdateCourseLocations = true; + [WaypointField] public static List CourseLocations; + public static int highestWaypointIndex = 0; + + void Awake() + { + if (Instance != null) + Destroy(Instance); + Instance = this; + WaypointField.Load(); + } + + void Start() + { + BDArmorySettings.WAYPOINT_COURSE_INDEX = Mathf.Clamp(BDArmorySettings.WAYPOINT_COURSE_INDEX, 0, CourseLocations.Count - 1); // Ensure the waypoint index is within limits. + highestWaypointIndex = CourseLocations.Max(c => c.waypoints.Count) - 1; + } + + void OnDestroy() + { + WaypointField.Save(); + } + } + + public class Waypoint + { + public string name; + public Vector3 location; + public float scale; + + public Waypoint(string _name, Vector3 _location, float _scale) { name = _name; location = _location; scale = _scale; } + public override string ToString() { return name + "| " + location.ToString("G6") + "| " + scale.ToString() + ": "; } + } + + public class WaypointCourse + { + public static string waypointLocationsCfg = Path.Combine(KSPUtil.ApplicationRootPath, "GameData/BDArmory/PluginData/Waypoint_locations.cfg"); + public string name; + public int worldIndex; + public Vector2 spawnPoint; + public List waypoints; + private string waypointList; + string GetWaypointList() + { + waypointList = string.Empty; + for (int i = 0; i < waypoints.Count; i++) + { + waypointList += waypoints[i].ToString(); + } + return waypointList; + } + //COURSE = TestCustom; 1; (23, 23); Start| (23.2, 23.2, 100)| 500: Funnel| (23.2, 23.7, 50)| 250: Ascent| (23.5, 23.6, 250)| 100: Apex| (23.2, 23.4 500)| 500: + public WaypointCourse(string _name, int _worldIndex, Vector2 _spawnPoint, List _waypoints) { name = _name; worldIndex = _worldIndex; spawnPoint = _spawnPoint; waypoints = _waypoints; } + public override string ToString() { return name + "; " + worldIndex + "; " + spawnPoint.ToString("G6") + "; " + GetWaypointList(); } + } + + [AttributeUsage(AttributeTargets.Field)] + public class WaypointField : Attribute + { + public WaypointField() { } + static List defaultLocations = new List{ + new WaypointCourse("Canyon", 1, new Vector2(27.97f, -39.35f), new List { + new Waypoint("Start", new Vector3(28.33f, -39.11f, 50), 200), + new Waypoint("Run-off Corner", new Vector3(28.83f, -38.06f, 50), 200), + new Waypoint("Careful River", new Vector3(29.54f, -38.68f, 50), 200), + new Waypoint("Lake of Mercy", new Vector3(30.15f, -38.6f, 50), 200), + new Waypoint("Danger Zone Narrows", new Vector3(30.83f, -38.87f, 50), 200), + new Waypoint("Chicane of Pain", new Vector3(30.73f, -39.6f, 50), 200), + new Waypoint("Bumpy Boi Lane", new Vector3(30.9f, -40.23f, 50), 200), + new Waypoint("Blaring Straights", new Vector3(30.83f, -41.26f, 50), 200) + }), + new WaypointCourse("Slalom", 1, new Vector2(-21.0158f, 72.2085f), new List { + new Waypoint("Waypoint 0", new Vector3(-21.0763f, 72.7194f, 100), 200), + new Waypoint("Waypoint 1", new Vector3(-21.3509f, 73.7466f, 100), 200), + new Waypoint("Waypoint 2", new Vector3(-20.8125f, 73.8125f, 100), 200), + new Waypoint("Waypoint 3", new Vector3(-20.6478f, 74.8177f, 100), 200), + new Waypoint("Waypoint 4", new Vector3(-20.2468f, 74.5046f, 100), 200), + new Waypoint("Waypoint 5", new Vector3(-19.7469f, 75.1252f, 100), 200), + new Waypoint("Waypoint 6", new Vector3(-19.2360f, 75.1363f, 100), 200), + new Waypoint("Waypoint 7", new Vector3(-18.8954f, 74.6530f, 100), 200) + }), + new WaypointCourse("Coast Circuit", 1, new Vector2(-7.7134f, -42.7633f), new List { + new Waypoint("Waypoint 0", new Vector3(-8.1628f, -42.7478f, 50), 200), + new Waypoint("Waypoint 1", new Vector3(-8.6737f, -42.7423f, 50), 200), + new Waypoint("Waypoint 2", new Vector3(-9.2230f, -42.5208f, 50), 200), + new Waypoint("Waypoint 3", new Vector3(-9.6624f, -43.3355f, 50), 200), + new Waypoint("Waypoint 4", new Vector3(-10.6732f, -43.3410f, 50), 200), + new Waypoint("Waypoint 5", new Vector3(-11.3379f, -42.9236f, 50), 200), + new Waypoint("Waypoint 6", new Vector3(-10.9415f, -42.3449f, 50), 200), + new Waypoint("Waypoint 7", new Vector3(-10.8591f, -41.8670f, 50), 200), + new Waypoint("Waypoint 8", new Vector3(-10.5515f, -41.6198f, 50), 200), + new Waypoint("Waypoint 9", new Vector3(-10.4746f, -41.2133f, 50), 200), + new Waypoint("Waypoint 10", new Vector3(-9.6945f, -41.2847f, 50), 200), + new Waypoint("Waypoint 11", new Vector3(-9.5407f, -42.1911f, 50), 200), + new Waypoint("Waypoint 12", new Vector3(-9.1342f, -42.0757f, 50), 200) + }) + }; + + public static void Save() + { + ConfigNode fileNode = ConfigNode.Load(WaypointCourse.waypointLocationsCfg); + if (fileNode == null) + fileNode = new ConfigNode(); + if (!fileNode.HasNode("Config")) + fileNode.AddNode("Config"); + + ConfigNode settings = fileNode.GetNode("Config"); + foreach (var field in typeof(WaypointCourses).GetFields(System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.DeclaredOnly)) + { + if (field == null || !field.IsDefined(typeof(WaypointField), false)) continue; + if (field.Name == "CourseLocations") continue; // We'll do the spawn locations separately. + var fieldValue = field.GetValue(null); + settings.SetValue(field.Name, field.GetValue(null).ToString(), true); + } + + if (!fileNode.HasNode("BDACourseLocations")) + fileNode.AddNode("BDACourseLocations"); + + ConfigNode CourseNode = fileNode.GetNode("BDACourseLocations"); + + CourseNode.ClearValues(); + foreach (var course in WaypointCourses.CourseLocations) + { + CourseNode.AddValue("COURSE", course.ToString()); + } + + if (!Directory.GetParent(WaypointCourse.waypointLocationsCfg).Exists) + { Directory.GetParent(WaypointCourse.waypointLocationsCfg).Create(); } + fileNode.Save(WaypointCourse.waypointLocationsCfg); + } + + public static void Load() + { + ConfigNode fileNode = ConfigNode.Load(WaypointCourse.waypointLocationsCfg); + + WaypointCourses.CourseLocations = new List(); + if (fileNode != null) + { + if (fileNode.HasNode("Config")) + { + ConfigNode settings = fileNode.GetNode("Config"); + foreach (var field in typeof(WaypointCourses).GetFields(System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.DeclaredOnly)) + { + if (field == null || !field.IsDefined(typeof(WaypointField), false)) continue; + if (field.Name == "CourseLocations") continue; // We'll do the spawn locations separately. + if (!settings.HasValue(field.Name)) continue; + object parsedValue = ParseValue(field.FieldType, settings.GetValue(field.Name)); + if (parsedValue != null) + { + field.SetValue(null, parsedValue); + } + } + } + + if (fileNode.HasNode("BDACourseLocations")) + { + ConfigNode settings = fileNode.GetNode("BDACourseLocations"); + foreach (var courseLocation in settings.GetValues("COURSE")) + { + var parsedValue = (WaypointCourse)ParseValue(typeof(WaypointCourse), courseLocation); + if (parsedValue != null) + { + WaypointCourses.CourseLocations.Add(parsedValue); + } + } + } + } + + // Add defaults if they're missing and we're not instructed not to. + if (WaypointCourses.UpdateCourseLocations) + { + foreach (var location in defaultLocations.ToList()) + if (!WaypointCourses.CourseLocations.Select(l => l.name).ToList().Contains(location.name)) + WaypointCourses.CourseLocations.Add(location); + } + } + + public static object ParseValue(Type type, string value) + { + try + { + if (type == typeof(string)) + { + return value; + } + else if (type == typeof(bool)) + { + return Boolean.Parse(value); + } + else if (type == typeof(int)) + { + return int.Parse(value); + } + else if (type == typeof(float)) + { + return float.Parse(value); + } + else if (type == typeof(Vector2)) + { + char[] charsToTrim = { '(', ')', ' ' }; + string[] strings = value.Trim(charsToTrim).Split(','); + if (strings.Length == 2) + { + float x = float.Parse(strings[0]); + float y = float.Parse(strings[1]); + return new Vector2(x, y); + } + } + else if (type == typeof(Vector3)) + { + char[] charsToTrim = { '[', ']', '(', ')', ' ' }; + string[] strings = value.Trim(charsToTrim).Split(','); + float x = float.Parse(strings[0]); + float y = float.Parse(strings[1]); + float z = float.Parse(strings[2]); + return new Vector3(x, y, z); + } + else if (type == typeof(WaypointCourse)) + { + string[] parts; + + parts = value.Split(new char[] { ';' }); + if (parts.Length > 1) + { + var name = (string)ParseValue(typeof(string), parts[0]); + var worldIndex = (int)ParseValue(typeof(int), parts[1]); + var spawnPoint = (Vector2)ParseValue(typeof(Vector2), parts[2]); + string[] waypoints = parts[3].Split(new char[] { ':' }); + List waypointList = new List(); + for (int i = 0; i < waypoints.Length - 1; i++) + { + string[] datavars; + datavars = waypoints[i].Split(new char[] { '|' }); + string WPname = (string)ParseValue(typeof(string), datavars[0]); + WPname = WPname.Trim(' '); + if (string.IsNullOrEmpty(WPname)) WPname = $"Waypoint {i}"; + var location = (Vector3)ParseValue(typeof(Vector3), datavars[1]); + var scale = (float)ParseValue(typeof(float), datavars[2]); + if (name != null && location != null) + waypointList.Add(new Waypoint(WPname, location, scale)); + } + + if (name != null && spawnPoint != null && waypointList.Count > 0) + return new WaypointCourse(name, worldIndex, spawnPoint, waypointList); + } + } + } + catch (Exception e) + { + Debug.LogException(e); + } + Debug.LogError("[BDArmory.WaypointCourses]: Failed to parse settings field of type " + type + " and value " + value); + return null; + } + } +} diff --git a/BDArmory/Guidances/BallisticGuidance.cs b/BDArmory/Guidances/BallisticGuidance.cs index 1cd311536..7df241c86 100644 --- a/BDArmory/Guidances/BallisticGuidance.cs +++ b/BDArmory/Guidances/BallisticGuidance.cs @@ -1,9 +1,11 @@ using System; -using BDArmory.Core.Extension; -using BDArmory.Misc; -using BDArmory.Modules; using UnityEngine; +using BDArmory.Extensions; +using BDArmory.Settings; +using BDArmory.Utils; +using BDArmory.Weapons.Missiles; + namespace BDArmory.Guidances { class BallisticGuidance : IGuidance @@ -35,12 +37,7 @@ public Vector3 GetDirection(MissileBase missile, Vector3 targetPosition, Vector3 if (missile.vessel.verticalSpeed > 0 && pendingDistance > _originalDistance * 0.5) { - missile.debugString.Append($"Ascending"); - missile.debugString.Append(Environment.NewLine); - var freeFallTime = CalculateFreeFallTime(missile); - missile.debugString.Append($"freeFallTime: {freeFallTime}"); - missile.debugString.Append(Environment.NewLine); var futureDistanceVector = Vector3 .Project((missile.vessel.GetFuturePosition() - _startPoint), (targetPosition - _startPoint).normalized); @@ -49,25 +46,22 @@ public Vector3 GetDirection(MissileBase missile, Vector3 targetPosition, Vector3 var horizontalTime = (_originalDistance - futureDistanceVector.magnitude) / futureHorizontalSpeed; - - missile.debugString.Append($"horizontalTime: {horizontalTime}"); - missile.debugString.Append(Environment.NewLine); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_MISSILES) + { + missile.debugString.AppendLine($"Ascending"); + missile.debugString.AppendLine($"freeFallTime: {freeFallTime}"); + missile.debugString.AppendLine($"horizontalTime: {horizontalTime}"); + } if (freeFallTime >= horizontalTime) { - missile.debugString.Append($"Free fall achieved:"); - missile.debugString.Append(Environment.NewLine); - + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_MISSILES) missile.debugString.AppendLine($"Free fall achieved:"); missile.Throttle = Mathf.Clamp(missile.Throttle - 0.001f, 0.01f, 1f); - } else { - missile.debugString.Append($"Free fall not achieved:"); - missile.debugString.Append(Environment.NewLine); - + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_MISSILES) missile.debugString.AppendLine($"Free fall not achieved:"); missile.Throttle = Mathf.Clamp(missile.Throttle + 0.001f, 0.01f, 1f); - } Vector3 dToTarget = targetPosition - missile.vessel.CoM; @@ -75,13 +69,11 @@ public Vector3 GetDirection(MissileBase missile, Vector3 targetPosition, Vector3 agmTarget = missile.vessel.CoM + direction; - missile.debugString.Append($"Throttle: {missile.Throttle}"); - missile.debugString.Append(Environment.NewLine); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_MISSILES) missile.debugString.AppendLine($"Throttle: {missile.Throttle}"); } else { - missile.debugString.Append($"Descending"); - missile.debugString.Append(Environment.NewLine); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_MISSILES) missile.debugString.AppendLine($"Descending"); agmTarget = MissileGuidance.GetAirToGroundTarget(targetPosition, targetVelocity, missile.vessel, 1.85f); missile.Throttle = Mathf.Clamp((float)(missile.vessel.atmDensity * 10f), 0.01f, 1f); @@ -108,13 +100,13 @@ private double CalculateFreeFallTime(MissileBase missile, int predictionTime = 1 double a = 9.80665f * missile.BallisticOverShootFactor; double d = missile.vessel.GetFutureAltitude(predictionTime); - double time1 = (-vi + Math.Sqrt(Math.Pow(vi, 2) - 4 * (0.5f * a) * (-d))) / a; - double time2 = (-vi - Math.Sqrt(Math.Pow(vi, 2) - 4 * (0.5f * a) * (-d))) / a; + double time1 = (-vi + Math.Sqrt(vi * vi - 4 * (0.5f * a) * (-d))) / a; + double time2 = (-vi - Math.Sqrt(vi * vi - 4 * (0.5f * a) * (-d))) / a; return Math.Max(time1, time2); } - private double CalculateFutureHorizontalSpeed(MissileBase missile,int predictionTime = 10) + private double CalculateFutureHorizontalSpeed(MissileBase missile, int predictionTime = 10) { return missile.vessel.horizontalSrfSpeed + (missile.HorizontalAcceleration / Time.fixedDeltaTime) * predictionTime; } diff --git a/BDArmory/Guidances/CruiseGuidance.cs b/BDArmory/Guidances/CruiseGuidance.cs index 290f86bc8..a88627516 100644 --- a/BDArmory/Guidances/CruiseGuidance.cs +++ b/BDArmory/Guidances/CruiseGuidance.cs @@ -1,10 +1,11 @@ using System; -using BDArmory.Core.Extension; -using BDArmory.Core.Utils; -using BDArmory.Misc; -using BDArmory.Modules; using UnityEngine; +using BDArmory.Extensions; +using BDArmory.Settings; +using BDArmory.Utils; +using BDArmory.Weapons.Missiles; + namespace BDArmory.Guidances { public enum GuidanceState @@ -68,19 +69,21 @@ public Vector3 GetDirection(MissileBase missile, Vector3 targetPosition, Vector3 upDirection = VectorUtils.GetUpDirection(_missile.vessel.CoM); - planarDirectionToTarget = - Vector3.ProjectOnPlane(targetPosition - _missile.vessel.CoM, upDirection).normalized; + planarDirectionToTarget = (targetPosition - _missile.vessel.CoM).ProjectOnPlanePreNormalized(upDirection).normalized; // Ascending - _missile.debugString.AppendLine("State=" + GuidanceState); var missileAltitude = GetCurrentAltitude(_missile.vessel); - _missile.debugString.AppendLine("Altitude=" + missileAltitude); - _missile.debugString.AppendLine("Apoapsis=" + _missile.vessel.orbit.ApA); - _missile.debugString.AppendLine("Future Altitude=" + _futureAltitude); - _missile.debugString.AppendLine("Pitch angle=" + _pitchAngle); - _missile.debugString.AppendLine("Pitch decision=" + PitchDecision); - _missile.debugString.AppendLine("lastVerticalSpeed=" + _lastVerticalSpeed); - _missile.debugString.AppendLine("verticalAcceleration=" + _verticalAcceleration); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_MISSILES) + { + _missile.debugString.AppendLine("State=" + GuidanceState); + _missile.debugString.AppendLine("Altitude=" + missileAltitude); + _missile.debugString.AppendLine("Apoapsis=" + _missile.vessel.orbit.ApA); + _missile.debugString.AppendLine("Future Altitude=" + _futureAltitude); + _missile.debugString.AppendLine("Pitch angle=" + _pitchAngle); + _missile.debugString.AppendLine("Pitch decision=" + PitchDecision); + _missile.debugString.AppendLine("lastVerticalSpeed=" + _lastVerticalSpeed); + _missile.debugString.AppendLine("verticalAcceleration=" + _verticalAcceleration); + } GetTelemetryData(); @@ -112,8 +115,7 @@ public Vector3 GetDirection(MissileBase missile, Vector3 targetPosition, Vector3 case GuidanceState.Terminal: - _missile.debugString.Append($"Descending"); - _missile.debugString.Append(Environment.NewLine); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_MISSILES) _missile.debugString.AppendLine($"Descending"); _missile.Throttle = Mathf.Clamp((float)(_missile.vessel.atmDensity * 10f), 0.01f, 1f); @@ -133,8 +135,8 @@ private double CalculateFreeFallTime(double missileAltitude) double a = 9.80665f; double d = missileAltitude; - double time1 = (-vi + Math.Sqrt(Math.Pow(vi, 2) - 4 * (0.5f * a) * (-d))) / a; - double time2 = (-vi - Math.Sqrt(Math.Pow(vi, 2) - 4 * (0.5f * a) * (-d))) / a; + double time1 = (-vi + Math.Sqrt(vi * vi - 4 * (0.5f * a) * (-d))) / a; + double time2 = (-vi - Math.Sqrt(vi * vi - 4 * (0.5f * a) * (-d))) / a; return Math.Max(time1, time2); } @@ -164,12 +166,13 @@ private bool CheckIfTerminal(double altitude, Vector3 targetPosition, Vector3 up float distanceToTarget = Vector3.Distance(surfacePos, targetPosition); - _missile.debugString.Append($"Distance to target" + distanceToTarget); - _missile.debugString.Append(Environment.NewLine); double freefallTime = CalculateFreeFallTime(altitude); - _missile.debugString.Append($"freefallTime" + freefallTime); - _missile.debugString.Append(Environment.NewLine); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_MISSILES) + { + _missile.debugString.AppendLine($"Distance to target" + distanceToTarget); + _missile.debugString.AppendLine($"freefallTime" + freefallTime); + } if (distanceToTarget < (freefallTime * _missile.vessel.horizontalSrfSpeed)) { @@ -314,7 +317,7 @@ private void MakeDecisionAboutPitch(MissileBase missile, double missileAltitude) private double CalculateFutureAltitude(float predictionTime) { Vector3 futurePosition = _missile.vessel.CoM + _missile.vessel.Velocity() * predictionTime - + 0.5f * _missile.vessel.acceleration_immediate * Math.Pow(predictionTime, 2); + + 0.5f * _missile.vessel.acceleration_immediate * predictionTime * predictionTime; return GetCurrentAltitudeAtPosition(futurePosition); } diff --git a/BDArmory/Guidances/IGuidance.cs b/BDArmory/Guidances/IGuidance.cs index ffa86fd90..b65a19554 100644 --- a/BDArmory/Guidances/IGuidance.cs +++ b/BDArmory/Guidances/IGuidance.cs @@ -1,5 +1,6 @@ -using BDArmory.Modules; -using UnityEngine; +using UnityEngine; + +using BDArmory.Weapons.Missiles; namespace BDArmory.Guidances { diff --git a/BDArmory/Guidances/MissileGuidance.cs b/BDArmory/Guidances/MissileGuidance.cs index b9dbe3734..c852c5bde 100644 --- a/BDArmory/Guidances/MissileGuidance.cs +++ b/BDArmory/Guidances/MissileGuidance.cs @@ -1,12 +1,11 @@ -using System; -using BDArmory.Control; -using BDArmory.Core; -using BDArmory.Core.Extension; -using BDArmory.Core.Utils; -using BDArmory.Misc; -using BDArmory.Modules; +using System; using UnityEngine; +using BDArmory.Extensions; +using BDArmory.Settings; +using BDArmory.Utils; +using BDArmory.Weapons.Missiles; + namespace BDArmory.Guidances { public class MissileGuidance @@ -16,7 +15,7 @@ public static Vector3 GetAirToGroundTarget(Vector3 targetPosition, Vector3 targe // Incorporate lead for target velocity Vector3 currVel = Mathf.Max((float)missileVessel.srfSpeed, minSpeed) * missileVessel.Velocity().normalized; float targetDistance = Vector3.Distance(targetPosition, missileVessel.transform.position); - float leadTime = Mathf.Clamp((float)(1 / ((targetVelocity - currVel).magnitude / targetDistance)), 0f, 8f); + float leadTime = Mathf.Clamp(targetDistance / (targetVelocity - currVel).magnitude, 0f, 8f); targetPosition += targetVelocity * leadTime; Vector3 upDirection = VectorUtils.GetUpDirection(missileVessel.CoM); @@ -40,10 +39,10 @@ public static Vector3 GetAirToGroundTarget(Vector3 targetPosition, Vector3 targe (distanceToTarget - ((float)missileVessel.srfSpeed * descentRatio)) * 0.22f, 0, (float)missileVessel.altitude); - //Debug.Log("AGM altitudeClamp =" + altitudeClamp); + //Debug.Log("[BDArmory.MissileGuidance]: AGM altitudeClamp =" + altitudeClamp); Vector3 finalTarget = targetPosition + (altitudeClamp * upDirection.normalized); - //Debug.Log("Using agm trajectory. " + Time.time); + //Debug.Log("[BDArmory.MissileGuidance]: Using agm trajectory. " + Time.time); return finalTarget; } @@ -52,7 +51,7 @@ public static bool GetBallisticGuidanceTarget(Vector3 targetPosition, Vessel mis out Vector3 finalTarget) { Vector3 up = VectorUtils.GetUpDirection(missileVessel.transform.position); - Vector3 forward = Vector3.ProjectOnPlane(targetPosition - missileVessel.transform.position, up); + Vector3 forward = (targetPosition - missileVessel.transform.position).ProjectOnPlanePreNormalized(up); float speed = (float)missileVessel.srfSpeed; float sqrSpeed = speed * speed; float sqrSpeedSqr = sqrSpeed * sqrSpeed; @@ -60,11 +59,11 @@ public static bool GetBallisticGuidanceTarget(Vector3 targetPosition, Vessel mis float height = FlightGlobals.getAltitudeAtPos(targetPosition) - FlightGlobals.getAltitudeAtPos(missileVessel.transform.position); float sqrRange = forward.sqrMagnitude; - float range = Mathf.Sqrt(sqrRange); + float range = BDAMath.Sqrt(sqrRange); float plusOrMinus = direct ? -1 : 1; - float top = sqrSpeed + (plusOrMinus * Mathf.Sqrt(sqrSpeedSqr - (g * ((g * sqrRange + (2 * height * sqrSpeed)))))); + float top = sqrSpeed + (plusOrMinus * BDAMath.Sqrt(sqrSpeedSqr - (g * ((g * sqrRange + (2 * height * sqrSpeed)))))); float bottom = g * range; float theta = Mathf.Atan(top / bottom); @@ -85,7 +84,7 @@ public static bool GetBallisticGuidanceTarget(Vector3 targetPosition, Vector3 mi float missileSpeed, bool direct, out Vector3 finalTarget) { Vector3 up = VectorUtils.GetUpDirection(missilePosition); - Vector3 forward = Vector3.ProjectOnPlane(targetPosition - missilePosition, up); + Vector3 forward = (targetPosition - missilePosition).ProjectOnPlanePreNormalized(up); float speed = missileSpeed; float sqrSpeed = speed * speed; float sqrSpeedSqr = sqrSpeed * sqrSpeed; @@ -93,11 +92,11 @@ public static bool GetBallisticGuidanceTarget(Vector3 targetPosition, Vector3 mi float height = FlightGlobals.getAltitudeAtPos(targetPosition) - FlightGlobals.getAltitudeAtPos(missilePosition); float sqrRange = forward.sqrMagnitude; - float range = Mathf.Sqrt(sqrRange); + float range = BDAMath.Sqrt(sqrRange); float plusOrMinus = direct ? -1 : 1; - float top = sqrSpeed + (plusOrMinus * Mathf.Sqrt(sqrSpeedSqr - (g * ((g * sqrRange + (2 * height * sqrSpeed)))))); + float top = sqrSpeed + (plusOrMinus * BDAMath.Sqrt(sqrSpeedSqr - (g * ((g * sqrRange + (2 * height * sqrSpeed)))))); float bottom = g * range; float theta = Mathf.Atan(top / bottom); @@ -127,7 +126,7 @@ public static Vector3 GetBeamRideTarget(Ray beam, Vector3 currentPosition, Vecto offset += beamVel * 0.5f; target += correctionFactor * offset; - Vector3 velDamp = correctionDamping * Vector3.ProjectOnPlane(currentVelocity - beamVel, beam.direction); + Vector3 velDamp = correctionDamping * (currentVelocity - beamVel).ProjectOnPlanePreNormalized(beam.direction); target -= velDamp; return target; @@ -141,23 +140,274 @@ public static Vector3 GetAirToAirTarget(Vector3 targetPosition, Vector3 targetVe Vector3 currVel = Mathf.Max((float)missileVessel.srfSpeed, minSpeed) * missileVessel.Velocity().normalized; - leadTime = (float)(1 / ((targetVelocity - currVel).magnitude / targetDistance)); + leadTime = targetDistance / (targetVelocity - currVel).magnitude; timeToImpact = leadTime; leadTime = Mathf.Clamp(leadTime, 0f, 8f); return targetPosition + (targetVelocity * leadTime); } + public static Vector3 GetAirToAirLoftTarget(Vector3 targetPosition, Vector3 targetVelocity, + Vector3 targetAcceleration, Vessel missileVessel, float targetAlt, float maxAltitude, + float rangeFactor, float vertVelComp, float velComp, float loftAngle, float termAngle, + float termDist, ref int loftState, out float timeToImpact, out float targetDistance, + MissileBase.GuidanceModes homingModeTerminal, float N, float minSpeed = 200) + { + Vector3 velDirection = missileVessel.srf_vel_direction; //missileVessel.Velocity().normalized; + + targetDistance = Vector3.Distance(targetPosition, missileVessel.transform.position); + + float currSpeed = Mathf.Max((float)missileVessel.srfSpeed, minSpeed); + Vector3 currVel = currSpeed * velDirection; + + //Vector3 Rdir = (targetPosition - missileVessel.transform.position).normalized; + //float rDot = Vector3.Dot(targetVelocity - currVel, Rdir); + + float leadTime = targetDistance / (targetVelocity - currVel).magnitude; + //float leadTime = (targetDistance / rDot); + + timeToImpact = leadTime; + leadTime = Mathf.Clamp(leadTime, 0f, 16f); + + // If loft is not terminal + if ((targetDistance > termDist) && (loftState < 3)) + { + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.MissileGuidance]: Lofting"); + + // Get up direction + Vector3 upDirection = VectorUtils.GetUpDirection(missileVessel.CoM); + + // Use the gun aim-assist logic to determine ballistic angle (assuming no drag) + Vector3 missileRelativePosition, missileRelativeVelocity, missileAcceleration, missileRelativeAcceleration, targetPredictedPosition, missileDropOffset, lastVelDirection, ballisticTarget, targetHorVel, targetCompVel; + + var firePosition = missileVessel.transform.position; //+ (currSpeed * velDirection) * Time.fixedDeltaTime; // Bullets are initially placed up to 1 frame ahead (iTime). Not offsetting by part vel gives the correct initial placement. + missileRelativePosition = targetPosition - firePosition; + float timeToCPA = timeToImpact; // Rough initial estimate. + targetPredictedPosition = AIUtils.PredictPosition(targetPosition, targetVelocity, targetAcceleration, timeToCPA); + + // Velocity Compensation Logic + float compMult = Mathf.Clamp(0.5f * (targetDistance - termDist) / termDist, 0f, 1f); + Vector3 velDirectionHor = (velDirection.ProjectOnPlanePreNormalized(upDirection)).normalized; //(velDirection - upDirection * Vector3.Dot(velDirection, upDirection)).normalized; + targetHorVel = targetVelocity.ProjectOnPlanePreNormalized(upDirection); //targetVelocity - upDirection * Vector3.Dot(targetVelocity, upDirection); // Get target horizontal velocity (relative to missile frame) + float targetAlVelMag = Vector3.Dot(targetHorVel, velDirectionHor); // Get magnitude of velocity aligned with the missile velocity vector (in the horizontal axis) + targetAlVelMag *= Mathf.Sign(velComp) * compMult; + targetAlVelMag = Mathf.Max(targetAlVelMag, 0f); //0.5f * (targetAlVelMag + Mathf.Abs(targetAlVelMag)); // Set -ve velocity (I.E. towards the missile) to 0 if velComp is +ve, otherwise for -ve + + float targetVertVelMag = Mathf.Max(0f, Mathf.Sign(vertVelComp) * compMult * Vector3.Dot(targetVelocity, upDirection)); + + //targetCompVel = targetVelocity + velComp * targetHorVel.magnitude* targetHorVel.normalized; // Old velComp logic + //targetCompVel = targetVelocity + velComp * targetAlVelMag * velDirectionHor; // New velComp logic + targetCompVel = targetVelocity + velComp * targetAlVelMag * velDirectionHor + vertVelComp * targetVertVelMag * upDirection; // New velComp logic + + var count = 0; + do + { + lastVelDirection = velDirection; + currVel = currSpeed * velDirection; + //firePosition = missileVessel.transform.position + (currSpeed * velDirection) * Time.fixedDeltaTime; // Bullets are initially placed up to 1 frame ahead (iTime). + missileAcceleration = FlightGlobals.getGeeForceAtPosition((firePosition + targetPredictedPosition) / 2f); // Drag is ignored. + //bulletRelativePosition = targetPosition - firePosition + compMult * altComp * upDirection; // Compensate for altitude + missileRelativePosition = targetPosition - firePosition; // Compensate for altitude + missileRelativeVelocity = targetVelocity - currVel; + missileRelativeAcceleration = targetAcceleration - missileAcceleration; + timeToCPA = AIUtils.TimeToCPA(missileRelativePosition, missileRelativeVelocity, missileRelativeAcceleration, timeToImpact * 3f); + targetPredictedPosition = AIUtils.PredictPosition(targetPosition, targetCompVel, targetAcceleration, timeToCPA); + missileDropOffset = -0.5f * missileAcceleration * timeToCPA * timeToCPA; + ballisticTarget = targetPredictedPosition + missileDropOffset; + velDirection = (ballisticTarget - missileVessel.transform.position).normalized; + } while (++count < 10 && Vector3.Angle(lastVelDirection, velDirection) > 1f); // 1° margin of error is sufficient to prevent premature firing (usually) + + + // Determine horizontal and up components of velocity, calculate the elevation angle + float velUp = Vector3.Dot(velDirection, upDirection); + float velForwards = (velDirection - upDirection * velUp).magnitude; + float angle = Mathf.Atan2(velUp, velForwards); + + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileGuidance]: Loft Angle: [{(angle * Mathf.Rad2Deg):G3}]"); + + // Use simple lead compensation to minimize over-compensation + // Get planar direction to target + Vector3 planarDirectionToTarget = + ((AIUtils.PredictPosition(targetPosition, targetVelocity, targetAcceleration, leadTime + TimeWarp.fixedDeltaTime) - missileVessel.transform.position).ProjectOnPlanePreNormalized(upDirection)).normalized; + + // Check if termination angle agrees with termAngle + if ((angle > -termAngle * Mathf.Deg2Rad) && (loftState < 2)) + { + /*// If not yet at termination, simple lead compensation + targetPosition += targetVelocity * leadTime + 0.5f * leadTime * leadTime * targetAcceleration; + + // Get planar direction to target + Vector3 planarDirectionToTarget = //(velDirection - upDirection * Vector3.Dot(velDirection, upDirection)).normalized; + ((targetPosition - missileVessel.transform.position).ProjectOnPlanePreNormalized(upDirection)).normalized;*/ + + // Altitude clamp based on rangeFactor and maxAlt, cannot be lower than target + float altitudeClamp = Mathf.Clamp(targetAlt + rangeFactor * Vector3.Dot(targetPosition - missileVessel.transform.position, planarDirectionToTarget), targetAlt, Mathf.Max(maxAltitude, targetAlt)); + + // Old loft climb logic, wanted to limit turn. Didn't work well but leaving it in if I decide to fix it + /*if (missileVessel.altitude < (altitudeClamp - 0.5f)) + //gain altitude if launching from stationary + {*/ + //currSpeed = (float)missileVessel.Velocity().magnitude; + + // 5g turn, v^2/r = a, v^2/(dh*(tan(45°/2)sin(45°))) > 5g, v^2/(tan(45°/2)sin(45°)) > 5g * dh, I.E. start turning when you need to pull a 5g turn, + // before that the required gs is lower, inversely proportional + /*if (loftState == 1 || (currSpeed * currSpeed * 0.2928932188134524755991556378951509607151640623115259634116f) >= (5f * (float)PhysicsGlobals.GravitationalAcceleration) * (altitudeClamp - missileVessel.altitude)) + {*/ + /* + loftState = 1; + + // Calculate upwards and forwards velocity components + velUp = Vector3.Dot(missileVessel.Velocity(), upDirection); + velForwards = (float)(missileVessel.Velocity() - upDirection * velUp).magnitude; + + // Derivation of relationship between dh and turn radius + // tan(theta/2) = dh/L, sin(theta) = L/r + // tan(theta/2) = sin(theta)/(1+cos(theta)) + float turnR = (float)(altitudeClamp - missileVessel.altitude) * (currSpeed * currSpeed + currSpeed * velForwards) / (velUp * velUp); + + float accel = Mathf.Clamp(currSpeed * currSpeed / turnR, 0, 5f * (float)PhysicsGlobals.GravitationalAcceleration); + */ + + // Limit climb angle by turnFactor, turnFactor goes negative when above target alt + float turnFactor = (float)(altitudeClamp - missileVessel.altitude) / (4f * (float)missileVessel.srfSpeed); + turnFactor = Mathf.Clamp(turnFactor, -1f, 1f); + + loftAngle = Mathf.Max(loftAngle, angle); + + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileGuidance]: AAM Loft altitudeClamp: [{altitudeClamp:G6}] COS: [{Mathf.Cos(loftAngle * turnFactor * Mathf.Deg2Rad):G3}], SIN: [{Mathf.Sin(loftAngle * turnFactor * Mathf.Deg2Rad):G3}], turnFactor: [{turnFactor:G3}]."); + return missileVessel.transform.position + (float)missileVessel.srfSpeed * ((Mathf.Cos(loftAngle * turnFactor * Mathf.Deg2Rad) * planarDirectionToTarget) + (Mathf.Sin(loftAngle * turnFactor * Mathf.Deg2Rad) * upDirection)); + + /* + Vector3 newVel = (velForwards * planarDirectionToTarget + velUp * upDirection); + //Vector3 accVec = Vector3.Cross(newVel, Vector3.Cross(upDirection, planarDirectionToTarget)); + Vector3 accVec = accel*(Vector3.Dot(newVel, planarDirectionToTarget) * upDirection - Vector3.Dot(newVel, upDirection) * planarDirectionToTarget).normalized; + + return missileVessel.transform.position + 1.5f * Time.fixedDeltaTime * newVel + 2.25f * Time.fixedDeltaTime * Time.fixedDeltaTime * accVec; + */ + /*} + return missileVessel.transform.position + 0.5f * (float)missileVessel.srfSpeed * ((Mathf.Cos(loftAngle * Mathf.Deg2Rad) * planarDirectionToTarget) + (Mathf.Sin(loftAngle * Mathf.Deg2Rad) * upDirection)); + */ + //} + + //Vector3 finalTarget = missileVessel.transform.position + 0.5f * (float)missileVessel.srfSpeed * planarDirectionToTarget + ((altitudeClamp - (float)missileVessel.altitude) * upDirection.normalized); + + //return finalTarget; + } + else + { + loftState = 2; + + // Tried to do some kind of pro-nav method. Didn't work well, leaving it just in case I want to fix it. + /* + Vector3 newVel = (float)missileVessel.srfSpeed * velDirection; + Vector3 accVec = (newVel - missileVessel.Velocity()); + Vector3 unitVel = missileVessel.Velocity().normalized; + accVec = accVec - unitVel * Vector3.Dot(unitVel, accVec); + + float accelTime = Mathf.Clamp(timeToImpact, 0f, 4f); + + accVec = accVec / accelTime; + + float accel = accVec.magnitude; + + if (accel > 20f * (float)PhysicsGlobals.GravitationalAcceleration) + { + accel = 20f * (float)PhysicsGlobals.GravitationalAcceleration / accel; + } + else + { + accel = 1f; + } + + Debug.Log("[BDArmory.MissileGuidance]: Loft: Diving, accel = " + accel); + return missileVessel.transform.position + 1.5f * Time.fixedDeltaTime * missileVessel.Velocity() + 2.25f * Time.fixedDeltaTime * Time.fixedDeltaTime * accVec * accel; + */ + + if (velUp > 0f) + { + /*return missileVessel.transform.position + (float)missileVessel.srfSpeed * new Vector3(velDirection.x - upDirection.x * velUp, + velDirection.y - upDirection.y * velUp, + velDirection.z - upDirection.z * velUp) + Mathf.Max(targetAlt - (float)missileVessel.altitude, 0f) * upDirection;*/ + return missileVessel.transform.position + (float)missileVessel.srfSpeed * planarDirectionToTarget + Mathf.Max(targetAlt - (float)missileVessel.altitude, 0f) * upDirection; + } + + //return missileVessel.transform.position + (float)missileVessel.srfSpeed * velDirection; + return missileVessel.transform.position + (float)missileVessel.srfSpeed * new Vector3(velUp * upDirection.x + velForwards * planarDirectionToTarget.x, + velUp * upDirection.y + velForwards * planarDirectionToTarget.y, + velUp * upDirection.z + velForwards * planarDirectionToTarget.z); + } + } + else + { + // If terminal just go straight for target + lead + loftState = 3; + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.MissileGuidance]: Terminal"); + + if (targetDistance < termDist) + { + if (homingModeTerminal == MissileBase.GuidanceModes.PN) + return GetPNTarget(targetPosition, targetVelocity, missileVessel, N, out timeToImpact); + else if (homingModeTerminal == MissileBase.GuidanceModes.APN) + return GetAPNTarget(targetPosition, targetVelocity, targetAcceleration, missileVessel, N, out timeToImpact); + else if (homingModeTerminal == MissileBase.GuidanceModes.AAMLead) + return AIUtils.PredictPosition(targetPosition, targetVelocity, targetAcceleration, leadTime + TimeWarp.fixedDeltaTime); + else if (homingModeTerminal == MissileBase.GuidanceModes.AAMPure) + return targetPosition; + else + return GetPNTarget(targetPosition, targetVelocity, missileVessel, N, out timeToImpact); // Default to PN + } + else + { + return AIUtils.PredictPosition(targetPosition, targetVelocity, targetAcceleration, leadTime + TimeWarp.fixedDeltaTime); //targetPosition + targetVelocity * leadTime + 0.5f * leadTime * leadTime * targetAcceleration; + //return targetPosition + targetVelocity * leadTime; + } + } + } + +/* public static Vector3 GetAirToAirHybridTarget(Vector3 targetPosition, Vector3 targetVelocity, + Vector3 targetAcceleration, Vessel missileVessel, float termDist, out float timeToImpact, + MissileBase.GuidanceModes homingModeTerminal, float N, float minSpeed = 200) + { + Vector3 velDirection = missileVessel.srf_vel_direction; //missileVessel.Velocity().normalized; + + float targetDistance = Vector3.Distance(targetPosition, missileVessel.transform.position); + + float currSpeed = Mathf.Max((float)missileVessel.srfSpeed, minSpeed); + Vector3 currVel = currSpeed * velDirection; + + float leadTime = targetDistance / (targetVelocity - currVel).magnitude; + + timeToImpact = leadTime; + leadTime = Mathf.Clamp(leadTime, 0f, 8f); + + if (targetDistance < termDist) + { + if (homingModeTerminal == MissileBase.GuidanceModes.APN) + return GetAPNTarget(targetPosition, targetVelocity, targetAcceleration, missileVessel, N, out timeToImpact); + else if (homingModeTerminal == MissileBase.GuidanceModes.PN) + return GetPNTarget(targetPosition, targetVelocity, missileVessel, N, out timeToImpact); + else if (homingModeTerminal == MissileBase.GuidanceModes.AAMPure) + return targetPosition; + else + return AIUtils.PredictPosition(targetPosition, targetVelocity, targetAcceleration, leadTime + TimeWarp.fixedDeltaTime); + } + else + { + return AIUtils.PredictPosition(targetPosition, targetVelocity, targetAcceleration, leadTime + TimeWarp.fixedDeltaTime); //targetPosition + targetVelocity * leadTime + 0.5f * leadTime * leadTime * targetAcceleration; + //return targetPosition + targetVelocity * leadTime; + } + }*/ + public static Vector3 GetAirToAirTargetModular(Vector3 targetPosition, Vector3 targetVelocity, Vector3 targetAcceleration, Vessel missileVessel, out float timeToImpact) { float targetDistance = Vector3.Distance(targetPosition, missileVessel.CoM); //Basic lead time calculation Vector3 currVel = ((float)missileVessel.srfSpeed * missileVessel.Velocity().normalized); - timeToImpact = (float)(1 / ((targetVelocity - currVel).magnitude / targetDistance)); + timeToImpact = targetDistance / (targetVelocity - currVel).magnitude; // Calculate time to CPA to determine target position - float timeToCPA = missileVessel.ClosestTimeToCPA(targetPosition, targetVelocity, targetAcceleration, 16f); + float timeToCPA = missileVessel.TimeToCPA(targetPosition, targetVelocity, targetAcceleration, 16f); timeToImpact = (timeToCPA < 16f) ? timeToCPA : timeToImpact; // Ease in velocity from 16s to 8s, ease in acceleration from 8s to 2s using the logistic function to give smooth adjustments to target point. float easeAccel = Mathf.Clamp01(1.1f / (1f + Mathf.Exp((timeToCPA - 5f))) - 0.05f); @@ -165,8 +415,45 @@ public static Vector3 GetAirToAirTargetModular(Vector3 targetPosition, Vector3 t return AIUtils.PredictPosition(targetPosition, targetVelocity * easeVel, targetAcceleration * easeAccel, timeToCPA + TimeWarp.fixedDeltaTime); // Compensate for the off-by-one frame issue. } + public static Vector3 GetPNTarget(Vector3 targetPosition, Vector3 targetVelocity, Vessel missileVessel, float N, out float timeToGo) + { + Vector3 missileVel = (float)missileVessel.srfSpeed * missileVessel.Velocity().normalized; + Vector3 relVelocity = targetVelocity - missileVel; + Vector3 relRange = targetPosition - missileVessel.CoM; + Vector3 RotVector = Vector3.Cross(relRange, relVelocity) / Vector3.Dot(relRange, relRange); + Vector3 RefVector = missileVel.normalized; + Vector3 normalAccel = -N * relVelocity.magnitude * Vector3.Cross(RefVector, RotVector); + timeToGo = missileVessel.TimeToCPA(targetPosition, targetVelocity, Vector3.zero, 120f); + return missileVessel.CoM + missileVel * timeToGo + normalAccel * timeToGo * timeToGo; + } + + public static Vector3 GetAPNTarget(Vector3 targetPosition, Vector3 targetVelocity, Vector3 targetAcceleration, Vessel missileVessel, float N, out float timeToGo) + { + Vector3 missileVel = (float)missileVessel.srfSpeed * missileVessel.Velocity().normalized; + Vector3 relVelocity = targetVelocity - missileVel; + Vector3 relRange = targetPosition - missileVessel.CoM; + Vector3 RotVector = Vector3.Cross(relRange, relVelocity) / Vector3.Dot(relRange, relRange); + Vector3 RefVector = missileVel.normalized; + Vector3 normalAccel = -N * relVelocity.magnitude * Vector3.Cross(RefVector, RotVector); + // float tgo = relRange.magnitude / relVelocity.magnitude; + Vector3 accelBias = Vector3.Cross(relRange.normalized, targetAcceleration); + accelBias = Vector3.Cross(RefVector, accelBias); + normalAccel -= 0.5f * N * accelBias; + timeToGo = missileVessel.TimeToCPA(targetPosition, targetVelocity, targetAcceleration, 120f); + return missileVessel.CoM + missileVel * timeToGo + normalAccel * timeToGo * timeToGo; + } + public static float GetLOSRate(Vector3 targetPosition, Vector3 targetVelocity, Vessel missileVessel) + { + Vector3 missileVel = (float)missileVessel.srfSpeed * missileVessel.Velocity().normalized; + Vector3 relVelocity = targetVelocity - missileVel; + Vector3 relRange = targetPosition - missileVessel.CoM; + Vector3 RotVector = Vector3.Cross(relRange, relVelocity) / Vector3.Dot(relRange, relRange); + Vector3 LOSRate = Mathf.Rad2Deg * RotVector; + return LOSRate.magnitude; + } + /// - /// Calculate a very accurate time to impact, use the out timeToimpact property if the method returned true. DEPRECIATED, use TimeToCPA. + /// Calculate a very accurate time to impact, use the out timeToimpact property if the method returned true. DEPRECATED, use TimeToCPA. /// /// /// @@ -207,7 +494,12 @@ private static bool CalculateAccurateTimeToImpact(float targetDistance, Vector3 return true; } - + /// + /// Air-2-Air fire solution used by the AI for steering, WM checking if a missile can be launched, unguided missiles + /// + /// + /// + /// public static Vector3 GetAirToAirFireSolution(MissileBase missile, Vessel targetVessel) { if (!targetVessel) @@ -218,47 +510,82 @@ public static Vector3 GetAirToAirFireSolution(MissileBase missile, Vessel target float leadTime = 0; float targetDistance = Vector3.Distance(targetVessel.transform.position, missile.transform.position); - Vector3 simMissileVel = 500 * (targetPosition - missile.transform.position).normalized; + //Vector3 simMissileVel = 500 * (targetPosition - missile.transform.position).normalized; MissileLauncher launcher = missile as MissileLauncher; + /* float optSpeed = 400; //TODO: Add parameter if (launcher != null) { - optSpeed = launcher.optimumAirspeed; + optSpeed = launcher.optimumAirspeed; //so it assumes missiles start out immediately possessing all their velocity instead of having to accelerate? That explains alot. } simMissileVel = optSpeed * (targetPosition - missile.transform.position).normalized; leadTime = targetDistance / (float)(targetVessel.Velocity() - simMissileVel).magnitude; leadTime = Mathf.Clamp(leadTime, 0f, 8f); + */ + Vector3 vel = missile.vessel.Velocity(); + Vector3 VelOpt = vel.normalized * (launcher != null ? launcher.optimumAirspeed : 1500); + float accel = launcher.thrust / missile.part.mass; + Vector3 deltaVel = targetVessel.Velocity() - vel; + Vector3 DeltaOptvel = targetVessel.Velocity() - VelOpt; + float T = Mathf.Clamp((VelOpt - vel).magnitude / accel, 0, 8); //time to optimal airspeed + + Vector3 relPosition = targetPosition - missile.transform.position; + Vector3 relAcceleration = targetVessel.acceleration - missile.GetForwardTransform() * accel; + leadTime = AIUtils.TimeToCPA(relPosition, deltaVel, relAcceleration, T); //missile accelerating, T is greater than our max look time of 8s + if (T < 8 && leadTime == T)//missile has reached max speed, and is now cruising; sim positions ahead based on T and run CPA from there + { + relPosition = AIUtils.PredictPosition(targetPosition, targetVessel.Velocity(), targetVessel.acceleration, T) - + AIUtils.PredictPosition(missile.transform.position, vel, missile.GetForwardTransform() * accel, T); + relAcceleration = targetVessel.acceleration; // - missile.MissileReferenceTransform.forward * 0; assume missile is holding steady velocity at optimumAirspeed + leadTime = AIUtils.TimeToCPA(relPosition, DeltaOptvel, relAcceleration, 8 - T) + T; + } + targetPosition = targetPosition + (targetVessel.Velocity() * leadTime); - if (targetVessel && targetDistance < 800) + if (targetVessel && targetDistance < 800) //TODO - investigate if this would throw off aim accuracy { targetPosition += (Vector3)targetVessel.acceleration * 0.05f * leadTime * leadTime; } return targetPosition; } - + /// + /// Air-2-Air lead offset calcualtion used for guided missiles + /// + /// + /// + /// + /// public static Vector3 GetAirToAirFireSolution(MissileBase missile, Vector3 targetPosition, Vector3 targetVelocity) { - float leadTime = 0; - float targetDistance = Vector3.Distance(targetPosition, missile.transform.position); - - float optSpeed = 400; //TODO: Add parameter MissileLauncher launcher = missile as MissileLauncher; - if (launcher != null) + float leadTime = 0; + Vector3 leadPosition = targetPosition; + Vector3 vel = missile.vessel.Velocity(); + Vector3 leadDirection, velOpt; + float accel = launcher.thrust / missile.part.mass; + float leadTimeError = 1f; + int count = 0; + do { - optSpeed = launcher.optimumAirspeed; - } - - Vector3 simMissileVel = optSpeed * (targetPosition - missile.transform.position).normalized; - leadTime = targetDistance / (targetVelocity - simMissileVel).magnitude; - leadTime = Mathf.Clamp(leadTime, 0f, 8f); - - targetPosition = targetPosition + (targetVelocity * leadTime); - - return targetPosition; + leadDirection = leadPosition - missile.transform.position; + float targetDistance = leadDirection.magnitude; + leadDirection.Normalize(); + velOpt = leadDirection * (launcher != null ? launcher.optimumAirspeed : 1500); + float deltaVel = Vector3.Dot(targetVelocity - vel, leadDirection); + float deltaVelOpt = Vector3.Dot(targetVelocity - velOpt, leadDirection); + float T = Mathf.Clamp((velOpt - vel).magnitude / accel, 0, 8); //time to optimal airspeed, clamped to at most 8s + float D = deltaVel * T + 1 / 2 * accel * (T * T); //relative distance covered accelerating to optimum airspeed + leadTimeError = -leadTime; + if (targetDistance > D) leadTime = (targetDistance - D) / deltaVelOpt + T; + else leadTime = (-deltaVel - BDAMath.Sqrt((deltaVel * deltaVel) + 2 * accel * targetDistance)) / accel; + leadTime = Mathf.Clamp(leadTime, 0f, 8f); + leadTimeError += leadTime; + leadPosition = AIUtils.PredictPosition(targetPosition, targetVelocity, Vector3.zero, leadTime); + } while (++count < 5 && Mathf.Abs(leadTimeError) > 1e-3f); // At most 5 iterations to converge. Also, 1e-2f may be sufficient. + return leadPosition; } public static Vector3 GetCruiseTarget(Vector3 targetPosition, Vessel missileVessel, float radarAlt) @@ -268,8 +595,7 @@ public static Vector3 GetCruiseTarget(Vector3 targetPosition, Vessel missileVess float distanceSqr = (targetPosition - (missileVessel.transform.position - (currentRadarAlt * upDirection))).sqrMagnitude; - Vector3 planarDirectionToTarget = - Vector3.ProjectOnPlane(targetPosition - missileVessel.transform.position, upDirection).normalized; + Vector3 planarDirectionToTarget = (targetPosition - missileVessel.transform.position).ProjectOnPlanePreNormalized(upDirection).normalized; float error; @@ -303,8 +629,7 @@ public static Vector3 GetCruiseTarget(Vector3 targetPosition, Vessel missileVess public static Vector3 GetTerminalManeuveringTarget(Vector3 targetPosition, Vessel missileVessel, float radarAlt) { Vector3 upDirection = -FlightGlobals.getGeeForceAtPosition(missileVessel.GetWorldPos3D()).normalized; - Vector3 planarVectorToTarget = Vector3.ProjectOnPlane(targetPosition - missileVessel.transform.position, - upDirection); + Vector3 planarVectorToTarget = (targetPosition - missileVessel.transform.position).ProjectOnPlanePreNormalized(upDirection); Vector3 planarDirectionToTarget = planarVectorToTarget.normalized; Vector3 crossAxis = Vector3.Cross(planarDirectionToTarget, upDirection).normalized; float sinAmplitude = Mathf.Clamp(Vector3.Distance(targetPosition, missileVessel.transform.position) - 850, 0, @@ -381,8 +706,8 @@ public static Vector3 DoAeroForces(MissileLauncher ml, Vector3 targetPosition, f float AoA = Mathf.Clamp(Vector3.Angle(ml.transform.forward, velocity.normalized), 0, 90); if (AoA > 0) { - double liftForce = 0.5 * airDensity * Math.Pow(airSpeed, 2) * liftArea * liftMultiplier * liftCurve.Evaluate(AoA); - Vector3 forceDirection = Vector3.ProjectOnPlane(-velocity, ml.transform.forward).normalized; + double liftForce = 0.5 * airDensity * airSpeed * airSpeed * liftArea * liftMultiplier * liftCurve.Evaluate(AoA); + Vector3 forceDirection = -velocity.ProjectOnPlanePreNormalized(ml.transform.forward).normalized; rb.AddForceAtPosition((float)liftForce * forceDirection, ml.transform.TransformPoint(ml.part.CoMOffset + CoL)); } @@ -390,7 +715,7 @@ public static Vector3 DoAeroForces(MissileLauncher ml, Vector3 targetPosition, f //drag if (airSpeed > 0) { - double dragForce = 0.5 * airDensity * Math.Pow(airSpeed, 2) * liftArea * dragMultiplier * dragCurve.Evaluate(AoA); + double dragForce = 0.5 * airDensity * airSpeed * airSpeed * liftArea * dragMultiplier * dragCurve.Evaluate(AoA); rb.AddForceAtPosition((float)dragForce * -velocity.normalized, ml.transform.TransformPoint(ml.part.CoMOffset + CoL)); } @@ -415,16 +740,14 @@ public static Vector3 DoAeroForces(MissileLauncher ml, Vector3 targetPosition, f torqueDirection = ml.transform.InverseTransformDirection(torqueDirection); float torque = Mathf.Clamp(targetAngle * steerMult, 0, maxTorque); - Vector3 finalTorque = Vector3.ProjectOnPlane(Vector3.Lerp(previousTorque, torqueDirection * torque, 1), - Vector3.forward); + Vector3 finalTorque = Vector3.Lerp(previousTorque, torqueDirection * torque, 1).ProjectOnPlanePreNormalized(Vector3.forward); rb.AddRelativeTorque(finalTorque); return finalTorque; } else { - Vector3 finalTorque = Vector3.ProjectOnPlane(Vector3.Lerp(previousTorque, Vector3.zero, 0.25f), - Vector3.forward); + Vector3 finalTorque = Vector3.Lerp(previousTorque, Vector3.zero, 0.25f).ProjectOnPlanePreNormalized(Vector3.forward); rb.AddRelativeTorque(finalTorque); return finalTorque; } diff --git a/BDArmory.Core/Bootstrapper.cs b/BDArmory/Initialization/Bootstrapper.cs similarity index 74% rename from BDArmory.Core/Bootstrapper.cs rename to BDArmory/Initialization/Bootstrapper.cs index 7ff908ecf..98e9b8a60 100644 --- a/BDArmory.Core/Bootstrapper.cs +++ b/BDArmory/Initialization/Bootstrapper.cs @@ -1,7 +1,8 @@ -using BDArmory.Core.Services; -using UnityEngine; +using UnityEngine; -namespace BDArmory.Core +using BDArmory.Damage; + +namespace BDArmory.Initialization { [KSPAddon(KSPAddon.Startup.Flight, false)] public class Bootstrapper : MonoBehaviour diff --git a/BDArmory/Initialization/Cleanup.cs b/BDArmory/Initialization/Cleanup.cs new file mode 100644 index 000000000..038c22807 --- /dev/null +++ b/BDArmory/Initialization/Cleanup.cs @@ -0,0 +1,48 @@ +using UnityEngine; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +using BDArmory.Competition; + +namespace BDArmory.Initialization +{ + [KSPAddon(KSPAddon.Startup.MainMenu, false)] + public class Cleanup : MonoBehaviour + { + bool hasRun = false; + bool inhibitAutoFunctions = false; + void Awake() + { + if (hasRun) return; + hasRun = true; + var BDArmoryCoreFiles = Directory.GetFiles(Path.GetFullPath(Path.Combine(KSPUtil.ApplicationRootPath, "GameData", "BDArmory", "Plugins"))).Where(f => Path.GetFileName(f).StartsWith("BDArmory.Core")).ToList(); + if (BDArmoryCoreFiles.Count > 0) + { + inhibitAutoFunctions = true; + var message = new List(); + message.Add("BDArmory has moved to using a single DLL. The following old BDArmory.Core files will be removed:"); + foreach (var BDArmoryCoreFile in BDArmoryCoreFiles) message.Add("\t" + BDArmoryCoreFile); + message.Add("Please restart KSP to avoid any potential issues."); + Debug.LogWarning(string.Join("\n", message.Select(s => "[BDArmory.Initialization]: " + s))); + PopupDialog.SpawnPopupDialog( + new Vector2(0.5f, 0.5f), + new Vector2(0.7f, 0.5f), // Seems to give a vertically centred dialog box with some width to show the longer strings. + "BDArmory Warning", + "BDArmory Warning", + string.Join("\n", message), + "OK", + false, + HighLogic.UISkin + ); + foreach (var BDArmoryCoreFile in BDArmoryCoreFiles) File.Delete(BDArmoryCoreFile); // Delete the BDArmory.Core files. + } + } + + void Start() + { + if (!inhibitAutoFunctions) return; + TournamentAutoResume.firstRun = false; // Prevents AUTO functionality from running once the level is loaded. + } + } +} \ No newline at end of file diff --git a/BDArmory.Core/Dependencies.cs b/BDArmory/Initialization/Dependencies.cs similarity index 97% rename from BDArmory.Core/Dependencies.cs rename to BDArmory/Initialization/Dependencies.cs index 465dcfb28..44d59e568 100644 --- a/BDArmory.Core/Dependencies.cs +++ b/BDArmory/Initialization/Dependencies.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -namespace BDArmory.Core +namespace BDArmory.Initialization { public static class Dependencies { diff --git a/BDArmory/Misc/MissileLaunchParams.cs b/BDArmory/Misc/MissileLaunchParams.cs deleted file mode 100644 index 194a17509..000000000 --- a/BDArmory/Misc/MissileLaunchParams.cs +++ /dev/null @@ -1,107 +0,0 @@ -using BDArmory.Core; -using BDArmory.Core.Extension; -using BDArmory.Modules; -using UnityEngine; - -namespace BDArmory.Misc -{ - public struct MissileLaunchParams - { - public float minLaunchRange; - public float maxLaunchRange; - - private float rtr; - - /// - /// Gets the maximum no-escape range. - /// - /// The max no-escape range. - public float rangeTr - { - get - { - return rtr; - } - } - - public MissileLaunchParams(float min, float max) - { - minLaunchRange = min; - maxLaunchRange = max; - rtr = (max + min) / 2; - } - - /// - /// Gets the dynamic launch parameters. - /// - /// The dynamic launch parameters. - /// Launcher velocity. - /// Target velocity. - public static MissileLaunchParams GetDynamicLaunchParams(MissileBase missile, Vector3 targetVelocity, Vector3 targetPosition) - { - Vector3 launcherVelocity = missile.vessel.Velocity(); - float launcherSpeed = (float)missile.vessel.srfSpeed; - float minLaunchRange = missile.minStaticLaunchRange; - float maxLaunchRange = missile.maxStaticLaunchRange; - - float bodyGravity = (float)PhysicsGlobals.GravitationalAcceleration * (float)missile.vessel.orbit.referenceBody.GeeASL; // Set gravity for calculations; - - float missileActiveTime = 2f; - - float rangeAddMin = 0; - float rangeAddMax = 0; - float relSpeed; - - // Calculate relative speed - Vector3 relV = targetVelocity - launcherVelocity; - Vector3 vectorToTarget = targetPosition - missile.part.transform.position; - Vector3 relVProjected = Vector3.Project(relV, vectorToTarget); - relSpeed = -Mathf.Sign(Vector3.Dot(relVProjected, vectorToTarget)) * relVProjected.magnitude; - - if (missile.GetComponent() == null) - { - // Basic time estimate for missile to drop and travel a safe distance from vessel assuming constant acceleration and firing vessel not accelerating - MissileLauncher ml = missile.GetComponent(); - float maxMissileAccel = ml.thrust / missile.part.mass; - float blastRadius = Mathf.Min(missile.GetBlastRadius(), 150f); // Allow missiles with absurd blast ranges to still be launched if desired - missileActiveTime = Mathf.Min((missile.vessel.LandedOrSplashed ? 0f : missile.dropTime) + Mathf.Sqrt(2 * blastRadius / maxMissileAccel), 2f); // Clamp at 2s for now - - // Rough range estimate of max missile G in a turn after launch, the following code is quite janky but works decently well in practice - float maxEstimatedGForce = Mathf.Max(bodyGravity * ml.maxTorque, 15f); // Rough estimate of max G based on missile torque, use minimum of 15G to prevent some VLS parts from not working - if (ml.aero) // If missile has aerodynamics, modify G force by AoA limit - { - maxEstimatedGForce *= Mathf.Sin(ml.maxAoA * Mathf.Deg2Rad); - } - - // Rough estimate of turning radius and arc length to travel - float arcLength = 0; - if ((!missile.vessel.LandedOrSplashed) && (missile.GetWeaponClass() != WeaponClasses.SLW) && (ml.guidanceActive)) // If the missile isn't a torpedo and has guidance - { - float futureTime = Mathf.Clamp((missile.vessel.LandedOrSplashed ? 0f : missile.dropTime), 0f, 2f); - Vector3 futureRelPosition = (targetPosition + targetVelocity * futureTime) - (missile.part.transform.position + launcherVelocity * futureTime); - float missileTurnRadius = (ml.optimumAirspeed * ml.optimumAirspeed) / maxEstimatedGForce; - float targetAngle = Vector3.Angle(missile.GetForwardTransform(), futureRelPosition); - arcLength = Mathf.Deg2Rad * targetAngle * missileTurnRadius; - } - - // Add additional range term for the missile to manuever to target at missileActiveTime - rangeAddMin += arcLength; - } - - float missileMaxRangeTime = 8f; // Placeholder value since this doesn't really matter much in BDA combat - - // Add to ranges - rangeAddMin += relSpeed * missileActiveTime; - rangeAddMax += relSpeed * missileMaxRangeTime; - - // Add altitude term to max - double diffAlt = missile.vessel.altitude - FlightGlobals.getAltitudeAtPos(targetPosition); - rangeAddMax += (float)diffAlt; - - float min = Mathf.Clamp(minLaunchRange + rangeAddMin, 0, BDArmorySettings.MAX_ENGAGEMENT_RANGE); - float max = Mathf.Clamp(maxLaunchRange + rangeAddMax, min + 100, BDArmorySettings.MAX_ENGAGEMENT_RANGE); - - return new MissileLaunchParams(min, max); - } - } -} diff --git a/BDArmory/Misc/Utils.cs b/BDArmory/Misc/Utils.cs deleted file mode 100644 index 2ee667bd4..000000000 --- a/BDArmory/Misc/Utils.cs +++ /dev/null @@ -1,442 +0,0 @@ -using BDArmory.Core.Extension; -using BDArmory.Core; -using BDArmory.Core.Utils; -using BDArmory.FX; -using BDArmory.Modules; -using BDArmory.UI; -using KSP.IO; -using KSP.UI.Screens; -using Object = UnityEngine.Object; -using System.Collections.Generic; -using System.Reflection; -using System; -using UniLinq; -using UnityEngine; - -namespace BDArmory.Misc -{ - public static class Utils - { - public static Texture2D resizeTexture = GameDatabase.Instance.GetTexture(BDArmorySetup.textureDir + "resizeSquare", false); - - public static Color ParseColor255(string color) - { - Color outputColor = new Color(0, 0, 0, 1); - - string[] strings = color.Split(","[0]); - for (int i = 0; i < 4; i++) - { - outputColor[i] = Single.Parse(strings[i]) / 255; - } - - return outputColor; - } - - public static AnimationState[] SetUpAnimation(string animationName, Part part) //Thanks Majiir! - { - List states = new List(); - using (IEnumerator animation = part.FindModelAnimators(animationName).AsEnumerable().GetEnumerator()) - while (animation.MoveNext()) - { - if (animation.Current == null) continue; - AnimationState animationState = animation.Current[animationName]; - animationState.speed = 0; // FIXME Shouldn't this be 1? - animationState.enabled = true; - animationState.wrapMode = WrapMode.ClampForever; - animation.Current.Blend(animationName); - states.Add(animationState); - } - return states.ToArray(); - } - - public static AnimationState SetUpSingleAnimation(string animationName, Part part) - { - using (IEnumerator animation = part.FindModelAnimators(animationName).AsEnumerable().GetEnumerator()) - while (animation.MoveNext()) - { - if (animation.Current == null) continue; - AnimationState animationState = animation.Current[animationName]; - animationState.speed = 0; // FIXME Shouldn't this be 1? - animationState.enabled = true; - animationState.wrapMode = WrapMode.ClampForever; - animation.Current.Blend(animationName); - return animationState; - } - return null; - } - - public static bool CheckMouseIsOnGui() - { - if (!BDArmorySetup.GAME_UI_ENABLED) return false; - - if (!BDInputSettingsFields.WEAP_FIRE_KEY.inputString.Contains("mouse")) return false; - - Vector3 inverseMousePos = new Vector3(Input.mousePosition.x, Screen.height - Input.mousePosition.y, 0); - Rect topGui = new Rect(0, 0, Screen.width, 65); - - if (topGui.Contains(inverseMousePos)) return true; - if (BDArmorySetup.windowBDAToolBarEnabled && BDArmorySetup.WindowRectToolbar.Contains(inverseMousePos)) - return true; - if (ModuleTargetingCamera.windowIsOpen && BDArmorySetup.WindowRectTargetingCam.Contains(inverseMousePos)) - return true; - if (BDArmorySetup.Instance.ActiveWeaponManager) - { - MissileFire wm = BDArmorySetup.Instance.ActiveWeaponManager; - - if (wm.vesselRadarData && wm.vesselRadarData.guiEnabled) - { - if (BDArmorySetup.WindowRectRadar.Contains(inverseMousePos)) return true; - if (wm.vesselRadarData.linkWindowOpen && wm.vesselRadarData.linkWindowRect.Contains(inverseMousePos)) - return true; - } - if (wm.rwr && wm.rwr.rwrEnabled && wm.rwr.displayRWR && BDArmorySetup.WindowRectRwr.Contains(inverseMousePos)) - return true; - if (wm.wingCommander && wm.wingCommander.showGUI) - { - if (BDArmorySetup.WindowRectWingCommander.Contains(inverseMousePos)) return true; - if (wm.wingCommander.showAGWindow && wm.wingCommander.agWindowRect.Contains(inverseMousePos)) - return true; - } - - if (extraGUIRects != null) - { - for (int i = 0; i < extraGUIRects.Count; i++) - { - if (extraGUIRects[i].Contains(inverseMousePos)) return true; - } - } - } - - return false; - } - - public static void ResizeGuiWindow(Rect windowrect, Vector2 mousePos) - { - } - - public static List extraGUIRects; - - public static int RegisterGUIRect(Rect rect) - { - if (extraGUIRects == null) - { - extraGUIRects = new List(); - } - - int index = extraGUIRects.Count; - extraGUIRects.Add(rect); - return index; - } - - public static void UpdateGUIRect(Rect rect, int index) - { - if (extraGUIRects == null) - { - Debug.LogWarning("[BDArmory.Misc]: Trying to update a GUI rect for mouse position check, but Rect list is null."); - } - - extraGUIRects[index] = rect; - } - - public static bool MouseIsInRect(Rect rect) - { - Vector3 inverseMousePos = new Vector3(Input.mousePosition.x, Screen.height - Input.mousePosition.y, 0); - return rect.Contains(inverseMousePos); - } - - //Thanks FlowerChild - //refreshes part action window - public static void RefreshAssociatedWindows(Part part) - { - IEnumerator window = Object.FindObjectsOfType(typeof(UIPartActionWindow)).Cast().GetEnumerator(); - while (window.MoveNext()) - { - if (window.Current == null) continue; - if (window.Current.part == part) - { - window.Current.displayDirty = true; - } - } - window.Dispose(); - } - - public static Vector3 ProjectOnPlane(Vector3 point, Vector3 planePoint, Vector3 planeNormal) - { - planeNormal = planeNormal.normalized; - - Plane plane = new Plane(planeNormal, planePoint); - float distance = plane.GetDistanceToPoint(point); - - return point - (distance * planeNormal); - } - - public static float SignedAngle(Vector3 fromDirection, Vector3 toDirection, Vector3 referenceRight) - { - float angle = Vector3.Angle(fromDirection, toDirection); - float sign = Mathf.Sign(Vector3.Dot(toDirection, referenceRight)); - float finalAngle = sign * angle; - return finalAngle; - } - - /// - /// Parses the string to a curve. - /// Format: "key:pair,key:pair" - /// - /// The curve. - /// Curve string. - public static FloatCurve ParseCurve(string curveString) - { - string[] pairs = curveString.Split(new char[] { ',' }); - Keyframe[] keys = new Keyframe[pairs.Length]; - for (int p = 0; p < pairs.Length; p++) - { - string[] pair = pairs[p].Split(new char[] { ':' }); - keys[p] = new Keyframe(float.Parse(pair[0]), float.Parse(pair[1])); - } - - FloatCurve curve = new FloatCurve(keys); - - return curve; - } - - private static int lineOfSightLayerMask = (int)(LayerMasks.Parts | LayerMasks.Scenery | LayerMasks.EVA | LayerMasks.Unknown19 | LayerMasks.Unknown23); - public static bool CheckSightLine(Vector3 origin, Vector3 target, float maxDistance, float threshold, - float startDistance) - { - float dist = maxDistance; - Ray ray = new Ray(origin, target - origin); - ray.origin += ray.direction * startDistance; - RaycastHit rayHit; - if (Physics.Raycast(ray, out rayHit, dist, lineOfSightLayerMask)) - { - if ((target - rayHit.point).sqrMagnitude < threshold * threshold) - { - return true; - } - else - { - return false; - } - } - - return false; - } - - public static bool CheckSightLineExactDistance(Vector3 origin, Vector3 target, float maxDistance, - float threshold, float startDistance) - { - float dist = maxDistance; - Ray ray = new Ray(origin, target - origin); - ray.origin += ray.direction * startDistance; - RaycastHit rayHit; - - if (Physics.Raycast(ray, out rayHit, dist, lineOfSightLayerMask)) - { - if ((target - rayHit.point).sqrMagnitude < threshold * threshold) - { - return true; - } - else - { - return false; - } - } - - return true; - } - - public static float[] ParseToFloatArray(string floatString) - { - string[] floatStrings = floatString.Split(new char[] { ',' }); - float[] floatArray = new float[floatStrings.Length]; - for (int i = 0; i < floatStrings.Length; i++) - { - floatArray[i] = float.Parse(floatStrings[i]); - } - - return floatArray; - } - - public static string FormattedGeoPos(Vector3d geoPos, bool altitude) - { - string finalString = string.Empty; - //lat - double lat = geoPos.x; - double latSign = Math.Sign(lat); - double latMajor = latSign * Math.Floor(Math.Abs(lat)); - double latMinor = 100 * (Math.Abs(lat) - Math.Abs(latMajor)); - string latString = latMajor.ToString("0") + " " + latMinor.ToString("0.000"); - finalString += "N:" + latString; - - //longi - double longi = geoPos.y; - double longiSign = Math.Sign(longi); - double longiMajor = longiSign * Math.Floor(Math.Abs(longi)); - double longiMinor = 100 * (Math.Abs(longi) - Math.Abs(longiMajor)); - string longiString = longiMajor.ToString("0") + " " + longiMinor.ToString("0.000"); - finalString += " E:" + longiString; - - if (altitude) - { - finalString += " ASL:" + geoPos.z.ToString("0.000"); - } - - return finalString; - } - - public static string FormattedGeoPosShort(Vector3d geoPos, bool altitude) - { - string finalString = string.Empty; - //lat - double lat = geoPos.x; - double latSign = Math.Sign(lat); - double latMajor = latSign * Math.Floor(Math.Abs(lat)); - double latMinor = 100 * (Math.Abs(lat) - Math.Abs(latMajor)); - string latString = latMajor.ToString("0") + " " + latMinor.ToString("0"); - finalString += "N:" + latString; - - //longi - double longi = geoPos.y; - double longiSign = Math.Sign(longi); - double longiMajor = longiSign * Math.Floor(Math.Abs(longi)); - double longiMinor = 100 * (Math.Abs(longi) - Math.Abs(longiMajor)); - string longiString = longiMajor.ToString("0") + " " + longiMinor.ToString("0"); - finalString += " E:" + longiString; - - if (altitude) - { - finalString += " ASL:" + geoPos.z.ToString("0"); - } - - return finalString; - } - - public static KeyBinding AGEnumToKeybinding(KSPActionGroup group) - { - string groupName = group.ToString(); - if (groupName.Contains("Custom")) - { - groupName = groupName.Substring(6); - int customNumber = int.Parse(groupName); - groupName = "CustomActionGroup" + customNumber; - } - else - { - return null; - } - - FieldInfo field = typeof(GameSettings).GetField(groupName); - return (KeyBinding)field.GetValue(null); - } - - public static float GetRadarAltitudeAtPos(Vector3 position, bool clamped = true) - { - double latitudeAtPos = FlightGlobals.currentMainBody.GetLatitude(position); - double longitudeAtPos = FlightGlobals.currentMainBody.GetLongitude(position); - float altitude = (float)(FlightGlobals.currentMainBody.GetAltitude(position)); - if (clamped) - return Mathf.Clamp(altitude - (float)FlightGlobals.currentMainBody.TerrainAltitude(latitudeAtPos, longitudeAtPos), 0, altitude); - else - return altitude - (float)FlightGlobals.currentMainBody.TerrainAltitude(latitudeAtPos, longitudeAtPos); - } - - public static string JsonCompat(string json) - { - return json.Replace('{', '<').Replace('}', '>'); - } - - public static string JsonDecompat(string json) - { - return json.Replace('<', '{').Replace('>', '}'); - } - - // this stupid thing makes all the BD armory parts explode - [KSPField] - private static string explModelPath = "BDArmory/Models/explosion/explosion"; - [KSPField] - public static string explSoundPath = "BDArmory/Sounds/explode1"; - - public static void ForceDeadVessel(Vessel v) - { - Debug.Log("[BDArmory.Misc]: GM Killed Vessel " + v.GetDisplayName()); - foreach (var missileFire in VesselModuleRegistry.GetModules(v)) - { - PartExploderSystem.AddPartToExplode(missileFire.part); - ExplosionFx.CreateExplosion(missileFire.part.transform.position, 1f, explModelPath, explSoundPath, ExplosionSourceType.Other, 0, missileFire.part); - } - } - - - // borrowed from SmartParts - activate the next stage on a vessel - public static void fireNextNonEmptyStage(Vessel v) - { - // the parts to be fired - List resultList = new List(); - - int highestNextStage = getHighestNextStage(v.rootPart, v.currentStage); - traverseChildren(v.rootPart, highestNextStage, ref resultList); - - foreach (Part stageItem in resultList) - { - //Log.Info("Activate:" + stageItem); - stageItem.activate(highestNextStage, stageItem.vessel); - stageItem.inverseStage = v.currentStage; - } - v.currentStage = highestNextStage; - //If this is the currently active vessel, activate the next, now empty, stage. This is an ugly, ugly hack but it's the only way to clear out the empty stage. - //Switching to a vessel that has been staged this way already clears out the empty stage, so this isn't required for those. - if (v.isActiveVessel) - { - StageManager.ActivateNextStage(); - } - } - - private static int getHighestNextStage(Part p, int currentStage) - { - - int highestChildStage = 0; - - // if this is the root part and its a decoupler: ignore it. It was probably fired before. - // This is dirty guesswork but everything else seems not to work. KSP staging is too messy. - if (p.vessel.rootPart == p && - (p.name.IndexOf("ecoupl") != -1 || p.name.IndexOf("eparat") != -1)) - { - } - else if (p.inverseStage < currentStage) - { - highestChildStage = p.inverseStage; - } - - - // Check all children. If this part has no children, inversestage or current Stage will be returned - int childStage = 0; - foreach (Part child in p.children) - { - childStage = getHighestNextStage(child, currentStage); - if (childStage > highestChildStage && childStage < currentStage) - { - highestChildStage = childStage; - } - } - return highestChildStage; - } - - private static void traverseChildren(Part p, int nextStage, ref List resultList) - { - if (p.inverseStage >= nextStage) - { - resultList.Add(p); - } - foreach (Part child in p.children) - { - traverseChildren(child, nextStage, ref resultList); - } - } - - public static float RoundToUnit(float value, float unit=1f) - { - var rounded = Mathf.Round(value / unit) * unit; - return (unit % 1 != 0) ? rounded : Mathf.Round(rounded); // Fix near-integer loss of precision. - } - } -} diff --git a/BDArmory/Misc/_description b/BDArmory/Misc/_description deleted file mode 100644 index f36f0963c..000000000 --- a/BDArmory/Misc/_description +++ /dev/null @@ -1,2 +0,0 @@ -Miscellaneous stuff that doesn't fit elsewhere. -- FIXME This ought to be cleaned up similarly to the Modules folder \ No newline at end of file diff --git a/BDArmory/ModIntegration/MouseAimFlight.cs b/BDArmory/ModIntegration/MouseAimFlight.cs new file mode 100644 index 000000000..a430d2f92 --- /dev/null +++ b/BDArmory/ModIntegration/MouseAimFlight.cs @@ -0,0 +1,106 @@ +using UnityEngine; +using System; +using System.Reflection; + +using BDArmory.Settings; + +namespace BDArmory.ModIntegration +{ + [KSPAddon(KSPAddon.Startup.Flight, false)] + public class MouseAimFlight : MonoBehaviour + { + public static MouseAimFlight Instance; + public static bool hasMouseAimFlight = false; + + Type mouseAimFlightType = null; + object mouseAimFlightInstance = null; + Func mouseAimFlightActiveFieldGetter = null; + bool mouseAimActive = false; + float lastChecked = 0; + Vessel activeVessel = null; + + void Awake() + { + if (Instance is not null) Destroy(Instance); + Instance = this; + } + + void Start() + { + FindMouseAimFlight(); + if (hasMouseAimFlight) + { + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.ModIntegration.MouseAimFlight]: MouseAimFlight mod detected."); + FindMouseAimFlightModule(); + } + else + { + Destroy(this); // Destroy ourselves to not take up any further CPU cycles. + } + } + + void FindMouseAimFlight() + { + try + { + foreach (var assy in AssemblyLoader.loadedAssemblies) + { + if (assy.assembly.FullName.Contains("MouseAimFlight")) + { + foreach (var type in assy.assembly.GetTypes()) + { + if (type == null) continue; + if (type.Name == "MouseAimVesselModule") + { + hasMouseAimFlight = true; + mouseAimFlightType = type; + foreach (var fieldInfo in mouseAimFlightType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance)) + { + if (fieldInfo != null && fieldInfo.Name == "mouseAimActive") + { + mouseAimFlightActiveFieldGetter = ReflectionUtils.CreateGetter(fieldInfo); + return; + } + } + } + } + } + } + } + catch (Exception e) + { + Debug.LogError($"[BDArmory.ModIntegration.MouseAimFlight]: Failed to locate mouseAimActive in MouseAimFlight module: {e.Message}"); + hasMouseAimFlight = false; + Destroy(this); + } + } + + void FindMouseAimFlightModule() + { + mouseAimFlightInstance = null; + activeVessel = FlightGlobals.ActiveVessel; + lastChecked = 0; + if (!hasMouseAimFlight || activeVessel == null) return; + mouseAimFlightInstance = (object)activeVessel.GetComponent(mouseAimFlightType); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.ModIntegration.MouseAimFlight]: Mouse Aim Flight module {(mouseAimFlightInstance != null ? "" : "not ")}found on {activeVessel.vesselName}"); + } + + bool CheckMouseAimActive() + { + lastChecked = Time.realtimeSinceStartup; + if (FlightGlobals.ActiveVessel != activeVessel) FindMouseAimFlightModule(); + if (mouseAimFlightInstance == null) return false; + return mouseAimFlightActiveFieldGetter(mouseAimFlightInstance); + } + + public bool IsMouseAimFlightActive() + { + if (!hasMouseAimFlight) return false; + if (FlightGlobals.ActiveVessel != activeVessel) FindMouseAimFlightModule(); + if (Time.realtimeSinceStartup - lastChecked > 1f) mouseAimActive = CheckMouseAimActive(); // Only check at most once per second unless a vessel switch occurs. + return mouseAimActive; + } + + public static bool IsMouseAimActive => hasMouseAimFlight && Instance != null && Instance.IsMouseAimFlightActive(); + } +} \ No newline at end of file diff --git a/BDArmory/ModIntegration/ReflectionUtils.cs b/BDArmory/ModIntegration/ReflectionUtils.cs new file mode 100644 index 000000000..29e92f6f0 --- /dev/null +++ b/BDArmory/ModIntegration/ReflectionUtils.cs @@ -0,0 +1,89 @@ +using System; +using System.Linq.Expressions; +using System.Reflection; +using System.Reflection.Emit; + +// Reflection Utils from CameraTools for faster accessing of other mods' fields. +namespace BDArmory.ModIntegration +{ + /// + /// Using delegates to speed up reflection for frequently accessed properties and fields. + /// This can give up to 1000x faster (but typically around 50-200x faster) access to these properties and fields. + /// https://stackoverflow.com/questions/10820453/reflection-performance-create-delegate-properties-c for properties. + /// https://stackoverflow.com/questions/16073091/is-there-a-way-to-create-a-delegate-to-get-and-set-values-for-a-fieldinfo for fields. + /// + public static class ReflectionUtils + { + public static Func BuildGetAccessor(MethodInfo method) + { + var obj = Expression.Parameter(typeof(object), "o"); + + Expression> expr = + Expression.Lambda>( + Expression.Convert( + Expression.Call( + method.IsStatic ? null : Expression.Convert(obj, method.DeclaringType), + method), + typeof(object)), + obj); + + return expr.Compile(); + } + + public static Action BuildSetAccessor(MethodInfo method) + { + var obj = Expression.Parameter(typeof(object), "o"); + var value = Expression.Parameter(typeof(object)); + + Expression> expr = + Expression.Lambda>( + Expression.Call( + method.IsStatic ? null : Expression.Convert(obj, method.DeclaringType), + method, + Expression.Convert(value, method.GetParameters()[0].ParameterType)), + obj, + value); + + return expr.Compile(); + } + + public static Func CreateGetter(FieldInfo field) + { + string methodName = field.ReflectedType.FullName + ".get_" + field.Name; + DynamicMethod getterMethod = new DynamicMethod(methodName, typeof(T), new Type[1] { typeof(S) }, true); + ILGenerator gen = getterMethod.GetILGenerator(); + if (field.IsStatic) + { + gen.Emit(OpCodes.Ldsfld, field); + } + else + { + gen.Emit(OpCodes.Ldarg_0); + gen.Emit(OpCodes.Ldfld, field); + } + gen.Emit(OpCodes.Ret); + return (Func)getterMethod.CreateDelegate(typeof(Func)); + } + + public static Action CreateSetter(FieldInfo field) + { + string methodName = field.ReflectedType.FullName + ".set_" + field.Name; + DynamicMethod setterMethod = new DynamicMethod(methodName, null, new Type[2] { typeof(S), typeof(T) }, true); + ILGenerator gen = setterMethod.GetILGenerator(); + if (field.IsStatic) + { + gen.Emit(OpCodes.Ldarg_1); + gen.Emit(OpCodes.Stsfld, field); + } + else + { + gen.Emit(OpCodes.Ldarg_0); + gen.Emit(OpCodes.Ldarg_1); + gen.Emit(OpCodes.Stfld, field); + } + gen.Emit(OpCodes.Ret); + return (Action)setterMethod.CreateDelegate(typeof(Action)); + } + + } +} \ No newline at end of file diff --git a/BDArmory/Modules/BDAAdjustableArmor.cs b/BDArmory/Modules/BDAAdjustableArmor.cs deleted file mode 100644 index d9da85b8b..000000000 --- a/BDArmory/Modules/BDAAdjustableArmor.cs +++ /dev/null @@ -1,104 +0,0 @@ -using BDArmory.Core.Module; -using System.Collections; -using System.Collections.Generic; -using UnityEngine; - -namespace BDArmory.Modules -{ - public class BDAAdjustableArmor : PartModule - { - [KSPField(isPersistant = true)] public float Height = 1; - - [KSPField(isPersistant = true)] public float Length = 1; - - [KSPField] - public string ArmorTransformName = "ArmorRootTransform"; - Transform armorTransform; - - HitpointTracker armor; - - public override void OnStart(StartState state) - { - armorTransform = part.FindModelTransform(ArmorTransformName); - - armorTransform.localScale = new Vector3(Height, Length, 1); - - if (HighLogic.LoadedSceneIsEditor) - { - armor = GetComponent(); - } - } - - [KSPEvent(guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_IncreaseHeight", active = true)]//Height ++ - public void IncreaseHeight() - { - Height = Mathf.Clamp(Height + 0.5f, 1f, 16f); - armorTransform.localScale = new Vector3(Height, Length, 1); - List.Enumerator sym = part.symmetryCounterparts.GetEnumerator(); - while (sym.MoveNext()) - { - if (sym.Current == null) continue; - sym.Current.FindModuleImplementing().UpdateScale(Height, Length); - updateArmorStats(); - } - sym.Dispose(); - } - - [KSPEvent(guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_DecreaseHeight", active = true)]//Height -- - public void DecreaseHeight() - { - Height = Mathf.Clamp(Height - 0.5f, 1f, 16f); - armorTransform.localScale = new Vector3(Height, Length, 1); - List.Enumerator sym = part.symmetryCounterparts.GetEnumerator(); - while (sym.MoveNext()) - { - if (sym.Current == null) continue; - sym.Current.FindModuleImplementing().UpdateScale(Height, Length); - updateArmorStats(); - } - sym.Dispose(); - } - - [KSPEvent(guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_IncreaseLength", active = true)]//Length ++ - public void IncreaseLength() - { - Length = Mathf.Clamp(Length + 0.5f, 1f, 16f); - armorTransform.localScale = new Vector3(Height, Length, 1); - List.Enumerator sym = part.symmetryCounterparts.GetEnumerator(); - while (sym.MoveNext()) - { - if (sym.Current == null) continue; - sym.Current.FindModuleImplementing().UpdateScale(Height, Length); - updateArmorStats(); - } - sym.Dispose(); - } - - [KSPEvent(guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_DecreaseLength", active = true)]//Length -- - public void DecreaseLength() - { - Length = Mathf.Clamp(Length - 0.5f, 1f, 16f); - armorTransform.localScale = new Vector3(Height, Length, 1); - List.Enumerator sym = part.symmetryCounterparts.GetEnumerator(); - while (sym.MoveNext()) - { - if (sym.Current == null) continue; - sym.Current.FindModuleImplementing().UpdateScale(Height, Length); - updateArmorStats(); - } - sym.Dispose(); - } - - public void UpdateScale(float height, float length) - { - Height = height; - Length = length; - armorTransform.localScale = new Vector3(Height, Length, 1); - updateArmorStats(); - } - public void updateArmorStats() - { - armor.ArmorSetup(null, null); - } - } -} diff --git a/BDArmory/Modules/BDAdjustableArmor.cs b/BDArmory/Modules/BDAdjustableArmor.cs deleted file mode 100644 index b617653af..000000000 --- a/BDArmory/Modules/BDAdjustableArmor.cs +++ /dev/null @@ -1,378 +0,0 @@ -using BDArmory.Core.Module; -using System.Collections; -using System.Collections.Generic; -using UnityEngine; - -namespace BDArmory.Modules -{ - public class BDAdjustableArmor : PartModule - { - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_ArmorWidth"),//Engage Range Min - UI_FloatRange(minValue = 0.5f, maxValue = 16, stepIncrement = 0.1f, scene = UI_Scene.Editor)] - public float Width = 1; - - private float OldWidth = 1; - - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_ArmorLength"),//Engage Range Min - UI_FloatRange(minValue = 0.5f, maxValue = 16, stepIncrement = 0.1f, scene = UI_Scene.Editor)] - public float Length = 1; - - private float OldLength = 1; - - [KSPField] - public bool isTriangularPanel = false; - - //public bool isCurvedPanel = false; - private float armorthickness = 1; - private float Oldthickness = 1; - - [KSPField] - public string ArmorTransformName = "ArmorTransform"; //transform of armor panel mesh/box collider - Transform[] armorTransforms; - - [KSPField] - public string ThicknessTransformName = "ThicknessTransform"; //name of armature to control thickness of curved panels - Transform ThicknessTransform; - - AttachNode N1; - AttachNode N2; - AttachNode N3; - AttachNode N4; - - [KSPField] - public string Node1Name = "Node1"; //name of attachnode node transform in model - Transform N1Transform; - [KSPField] - public string Node2Name = "Node2"; - Transform N2Transform; - [KSPField] - public string Node3Name = "Node3"; - Transform N3Transform; - [KSPField] - public string Node4Name = "Node4"; - Transform N4Transform; - - HitpointTracker armor; - - public override void OnStart(StartState state) - { - armorTransforms = part.FindModelTransforms(ArmorTransformName); - for (int i = 0; i < armorTransforms.Length; i++) - { - armorTransforms[i].localScale = new Vector3(Width, Length, 1); - } - ThicknessTransform = part.FindModelTransform(ThicknessTransformName); - //if (ThicknessTransform != null) - //{ - // isCurvedPanel = true; - //} - UI_FloatRange AWidth = (UI_FloatRange)Fields["Width"].uiControlEditor; - //if (isCurvedPanel) - //{ - // AWidth.maxValue = 4f; - // AWidth.minValue = 0f; - // AWidth.stepIncrement = 1; - // Fields["Width"].guiName = "#LOC_BDArmory_CylArmorScale"; - //} - AWidth.onFieldChanged = AdjustWidth; - UI_FloatRange ALength = (UI_FloatRange)Fields["Length"].uiControlEditor; - ALength.onFieldChanged = AdjustLength; - if (HighLogic.LoadedSceneIsEditor) - { - GameEvents.onEditorShipModified.Add(OnEditorShipModifiedEvent); - } - armor = GetComponent(); - N1 = part.FindAttachNode("Node1"); - N1.nodeType = AttachNode.NodeType.Stack; - N1Transform = part.FindModelTransform(Node1Name); - N2 = part.FindAttachNode("Node2"); - N2.nodeType = AttachNode.NodeType.Stack; - N2Transform = part.FindModelTransform(Node2Name); - if (!isTriangularPanel) - { - N3 = part.FindAttachNode("Node3"); - N3Transform = part.FindModelTransform(Node3Name); - N3.nodeType = AttachNode.NodeType.Stack; - N4 = part.FindAttachNode("Node4"); - N4Transform = part.FindModelTransform(Node4Name); - N4.nodeType = AttachNode.NodeType.Stack; - } - UpdateThickness(true); - } - public override void OnLoad(ConfigNode node) - { - base.OnLoad(node); - - if (!HighLogic.LoadedSceneIsEditor && !HighLogic.LoadedSceneIsFlight) return; - armor = GetComponent(); - } - private void OnDestroy() - { - if (HighLogic.LoadedSceneIsEditor) - { - GameEvents.onEditorShipModified.Remove(OnEditorShipModifiedEvent); - } - } - - public void AdjustWidth(BaseField field, object obj) - { - Width = Mathf.Clamp(Width, 0.5f, 16f); - //if (!isCurvedPanel) - { - for (int i = 0; i < armorTransforms.Length; i++) - { - armorTransforms[i].localScale = new Vector3(Width, Length, armorthickness); - } - using (List.Enumerator sym = part.symmetryCounterparts.GetEnumerator()) - while (sym.MoveNext()) - { - if (sym.Current == null) continue; - sym.Current.FindModuleImplementing().UpdateScale(Width, Length); //needs to be changed to use updatewitth() - FIXME later, future SI - } - } - /* - else - { - for (int i = 0; i < armorTransforms.Length; i++) - { - armorTransforms[i].localScale = new Vector3(Width, Length, Width); - } - using (List.Enumerator sym = part.symmetryCounterparts.GetEnumerator()) - while (sym.MoveNext()) - { - if (sym.Current == null) continue; - sym.Current.FindModuleImplementing().UpdateScale(Width, Length); - } - } - */ - updateArmorStats(); - HandleWidthChange(Width, OldWidth); - OldWidth = Width; - } - public void AdjustLength(BaseField field, object obj) - { - Length = Mathf.Clamp(Length, 0.5f, 16f); - for (int i = 0; i < armorTransforms.Length; i++) - { - armorTransforms[i].localScale = new Vector3(Width, Length, armorthickness); - } - using (List.Enumerator sym = part.symmetryCounterparts.GetEnumerator()) - while (sym.MoveNext()) - { - if (sym.Current == null) continue; - sym.Current.FindModuleImplementing().UpdateScale(Width, Length); - } - updateArmorStats(); - HandleLengthChange(Length, OldLength); - OldLength = Length; - } - - public void UpdateScale(float width, float length) - { - Width = width; - Length = length; - - //if (!isCurvedPanel) - { - for (int i = 0; i < armorTransforms.Length; i++) - { - armorTransforms[i].localScale = new Vector3(Width, Length, Mathf.Clamp((armor.Armor / 10), 0.1f, 1500)); - } - } - /* - else - { - for (int i = 0; i < armorTransforms.Length; i++) - { - armorTransforms[i].localScale = new Vector3(Width, Length, Width); - } - } - */ - updateArmorStats(); - HandleWidthChange(Width, OldWidth); - HandleLengthChange(Length, OldLength); - } - - /// ////////////////////////////////// - //Borrowed/modified from ProceduralParts - public virtual void HandleLengthChange(float length, float oldLength) - { - float trans = length - oldLength; - - N1.position.z = N1.position.z + (trans / 2); - if (N1.attachedPart is Part N1pushTarget) - { - TranslatePart(N1pushTarget, N1Transform.forward * (trans / 2)); - } - N1.size = Mathf.CeilToInt(length / 2); - N1.breakingForce = length * 100; - N1.breakingTorque = length * 100; - if (!isTriangularPanel) - { - N3.position.z = N3.position.z + (-trans / 2); - if (N3.attachedPart is Part N3pushTarget) - { - TranslatePart(N3pushTarget, -N3Transform.forward * (trans / 2)); - } - N3.size = Mathf.CeilToInt(length / 2); - N3.breakingForce = length * 100; - N3.breakingTorque = length * 100; - } - foreach (Part p in part.children) - { - if (p.FindAttachNodeByPart(part) is AttachNode node && node.nodeType == AttachNode.NodeType.Surface) - { - Vector3 localSpace = part.transform.InverseTransformPoint(node.owner.transform.TransformPoint(node.position)); - if (localSpace.z > 0.9f * (oldLength / 2)) - { - //Debug.Log("[BDAA DEBUG] srfAttack detected on N1 edge, moving"); - TranslatePart(p, N1Transform.up * (trans / 2)); - } - if (!isTriangularPanel) - { - if (localSpace.z < 0.9f * -(oldLength / 2)) - { - //Debug.Log("[BDAA DEBUG] srfAttack detected on N3 edge, moving"); - TranslatePart(p, -N3Transform.up * (trans / 2)); - } - } - } - } - } - - public virtual void HandleWidthChange(float width, float oldWidth) - { - float trans = width - oldWidth; - N2.position.x = N2.position.x + (-trans / 2); - if (N2.attachedPart is Part N2pushTarget) - { - TranslatePart(N2pushTarget, -N2Transform.right * (trans / 2)); - } - N2.size = Mathf.CeilToInt(width / 2); - N2.breakingForce = width * 100; - N2.breakingTorque = width * 100; - if (!isTriangularPanel) - { - N4.position.x = N4.position.x + (trans / 2); - if (N4.attachedPart is Part N4pushTarget) - { - TranslatePart(N4pushTarget, N4Transform.right * (trans / 2)); - } - N4.size = Mathf.CeilToInt(width / 2); - N4.breakingForce = width * 100; - N4.breakingTorque = width * 100; - } - - foreach (Part p in part.children) - { - if (p.FindAttachNodeByPart(part) is AttachNode node && node.nodeType == AttachNode.NodeType.Surface) - { - Vector3 localSpace = part.transform.InverseTransformPoint(node.owner.transform.TransformPoint(node.position)); - if (localSpace.x < 0.9f * -(oldWidth / 2)) - { - TranslatePart(p, N2Transform.right * (trans / 2)); - } - if (!isTriangularPanel) - { - if (localSpace.x > 0.9f * (oldWidth / 2)) - { - TranslatePart(p, N4Transform.right * (trans / 2)); - } - } - } - } - } - - public Part GetEldestParent(Part p) => (p.parent is null) ? p : GetEldestParent(p.parent); - public void TranslatePart(Part pushTarget, Vector3 translation) - { - // If the attached part is a child of ours, push it directly. - // If it is our parent, then we need to find the eldest grandparent and push that, and also ourselves - if (pushTarget == this.part.parent) - { - this.part.transform.Translate(-translation, Space.Self); // Push ourselves normally - float sibMult = part.symmetryCounterparts == null ? 1f : 1f / (part.symmetryCounterparts.Count + 1); - pushTarget = GetEldestParent(this.part); - translation *= sibMult; // Push once for each symmetry sibling, so scale the parent push. - } - // Convert to world space, to deal with bizarre orientation relationships. - // (ex: pushTarget is inverted, and our top node connects to its top node) - Vector3 worldSpaceTranslation = part.transform.TransformVector(translation); //FIXME - fix transforms to yield relative to part, not SPH - pushTarget.transform.Translate(worldSpaceTranslation, Space.World); - } - /// /////////////////////////// - - public void updateArmorStats() - { - /* - if (isCurvedPanel) - { - armor.armorVolume = (Length * (Mathf.Clamp(Width, 0.5f, 4) * Mathf.Clamp(Width, 0.5f, 4))); //gives surface area for 1/4 cyl srf area - } - else - */ - { - armor.armorVolume = (Width * Length); - if (isTriangularPanel) - { - armor.armorVolume /= 2; - } - } - armor.ArmorSetup(null, null); - } - void UpdateThickness(bool onLoad = false) - { - if (armor != null && armorTransforms != null) - { - armorthickness = Mathf.Clamp((armor.Armor / 10), 0.1f, 1500); - //if (!isCurvedPanel) - { - if (armorthickness != Oldthickness) - { - for (int i = 0; i < armorTransforms.Length; i++) - { - armorTransforms[i].localScale = new Vector3(Width, Length, armorthickness); - } - } - } - /* - else - { - ThicknessTransform.localScale = new Vector3(armorthickness, 1, armorthickness); - } - */ - } - else - { - //if (armor == null) Debug.Log("[BDAAdjustableArmor] No HitpointTracker found! aborting UpdateThickness()!"); - //if (armorTransforms == null) Debug.Log("[BDAAdjustableArmor] No ArmorTransform found! aborting UpdateThickness()!"); - return; - } - if (onLoad) return; //don't adjust part placement on load - if (armorthickness != Oldthickness) - { - float ratio = (armorthickness - Oldthickness) / 100; - foreach (Part p in part.children) - { - if (p.FindAttachNodeByPart(part) is AttachNode node && node.nodeType == AttachNode.NodeType.Surface) - { - Vector3 localSpace = part.transform.InverseTransformPoint(node.owner.transform.TransformPoint(node.position)); - if (localSpace.y > (0.9f * ratio)) - { - TranslatePart(p, -armorTransforms[0].right * ratio); - } - if (localSpace.y < -(0.9f * ratio)) - { - TranslatePart(p, armorTransforms[0].right * ratio); - } - } - } - Oldthickness = armorthickness; - } - } - private void OnEditorShipModifiedEvent(ShipConstruct data) - { - UpdateThickness(); - } - } -} \ No newline at end of file diff --git a/BDArmory/Modules/KerbalSafety.cs b/BDArmory/Modules/KerbalSafety.cs index 0ad8daa54..f6ece7641 100644 --- a/BDArmory/Modules/KerbalSafety.cs +++ b/BDArmory/Modules/KerbalSafety.cs @@ -1,11 +1,14 @@ +using System; using System.Collections; using System.Collections.Generic; using System.Linq; using UnityEngine; -using BDArmory.Core; -using BDArmory.Core.Extension; + using BDArmory.Competition; +using BDArmory.Extensions; +using BDArmory.Settings; using BDArmory.UI; +using BDArmory.Utils; namespace BDArmory.Modules { @@ -106,6 +109,7 @@ public void CheckVesselForKerbals(Vessel vessel) foreach (var part in vessel.parts) { if (part == null) continue; + if (part.IsKerbalSeat()) continue; // Ignore the seat, which gives a false positive below. foreach (var crew in part.protoModuleCrew) { if (crew == null) continue; @@ -152,7 +156,7 @@ IEnumerator ManageNewlyEjectedKerbalCoroutine(KerbalEVA kerbal) /// The amount of real-time to manually update for. IEnumerator ManuallyMoveKerbalEVACoroutine(KerbalEVA kerbal, Vector3 velocity, float realTime = 1f) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.KerbalSafety]: Manually setting position of " + kerbal.vessel.vesselName + " for " + realTime + "s of real-time."); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log("[BDArmory.KerbalSafety]: Manually setting position of " + kerbal.vessel.vesselName + " for " + realTime + "s of real-time."); if (!evaKerbalsToMonitor.Contains(kerbal)) evaKerbalsToMonitor.Add(kerbal); var gee = (Vector3)FlightGlobals.getGeeForceAtPosition(kerbal.transform.position); var verticalSpeed = Vector3.Dot(-gee.normalized, velocity); @@ -161,8 +165,8 @@ IEnumerator ManuallyMoveKerbalEVACoroutine(KerbalEVA kerbal, Vector3 velocity, f var position = kerbal.vessel.GetWorldPos3D(); if (kerbal.vessel.radarAltitude + verticalSpeed * Time.fixedDeltaTime < 2f) // Crashed into terrain, explode upwards. { - if (BDArmorySettings.DRAW_DEBUG_LABELS) verticalSpeedAdjustment = 3f * (float)gee.magnitude - verticalSpeed; - velocity = Vector3.ProjectOnPlane(velocity, -gee.normalized) - 3f * (gee + UnityEngine.Random.onUnitSphere * 0.3f * gee.magnitude); + if (BDArmorySettings.DEBUG_OTHER) verticalSpeedAdjustment = 3f * (float)gee.magnitude - verticalSpeed; + velocity = velocity.ProjectOnPlanePreNormalized(-gee.normalized) - 3f * (gee + UnityEngine.Random.onUnitSphere * 0.3f * gee.magnitude); position += (2f - (float)kerbal.vessel.radarAltitude) * -gee.normalized; kerbal.vessel.SetPosition(position); // Put the kerbal back at just above gound level. kerbal.vessel.Landed = false; @@ -170,12 +174,12 @@ IEnumerator ManuallyMoveKerbalEVACoroutine(KerbalEVA kerbal, Vector3 velocity, f else { velocity += 1.5f * -(gee + UnityEngine.Random.onUnitSphere * 0.3f * gee.magnitude); - if (BDArmorySettings.DRAW_DEBUG_LABELS) verticalSpeedAdjustment = 1.5f * (float)gee.magnitude; + if (BDArmorySettings.DEBUG_OTHER) verticalSpeedAdjustment = 1.5f * (float)gee.magnitude; } verticalSpeed = Vector3.Dot(-gee.normalized, velocity); kerbal.vessel.SetRotation(UnityEngine.Random.rotation); kerbal.vessel.rootPart.AddTorque(UnityEngine.Random.onUnitSphere * UnityEngine.Random.Range(1, 2)); - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log($"[BDArmory.KerbalSafety]: Setting {kerbal.vessel.vesselName}'s position to {position:F2} ({kerbal.vessel.GetWorldPos3D():F2}, altitude: {kerbal.vessel.radarAltitude:F2}, {kerbal.vessel.altitude:F2}) and velocity to {velocity.magnitude:F2} ({verticalSpeed:F2}m/s vertically, adjusted by {verticalSpeedAdjustment:F2}m/s)"); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.KerbalSafety]: Setting {kerbal.vessel.vesselName}'s position to {position:F2} ({kerbal.vessel.GetWorldPos3D():F2}, altitude: {kerbal.vessel.radarAltitude:F2}, {kerbal.vessel.altitude:F2}) and velocity to {velocity.magnitude:F2} ({verticalSpeed:F2}m/s vertically, adjusted by {verticalSpeedAdjustment:F2}m/s)"); var startTime = Time.realtimeSinceStartup; kerbal.vessel.rootPart.SetDetectCollisions(false); while (kerbal != null && kerbal.isActiveAndEnabled && kerbal.vessel != null && kerbal.vessel.isActiveAndEnabled && Time.realtimeSinceStartup - startTime < realTime) @@ -184,18 +188,18 @@ IEnumerator ManuallyMoveKerbalEVACoroutine(KerbalEVA kerbal, Vector3 velocity, f if (verticalSpeed < 0f && kerbal.vessel.radarAltitude + verticalSpeed * (realTime - (Time.realtimeSinceStartup - startTime)) < 100f) { velocity = velocity * 0.968f + gee * verticalSpeed / 10f * Time.fixedDeltaTime; - if (BDArmorySettings.DRAW_DEBUG_LABELS) verticalSpeedAdjustment = Vector3.Dot(-gee.normalized, gee * verticalSpeed / 10f * Time.fixedDeltaTime); + if (BDArmorySettings.DEBUG_OTHER) verticalSpeedAdjustment = Vector3.Dot(-gee.normalized, gee * verticalSpeed / 10f * Time.fixedDeltaTime); } else { velocity = velocity * 0.968f + gee * Time.fixedDeltaTime; - if (BDArmorySettings.DRAW_DEBUG_LABELS) verticalSpeedAdjustment = Vector3.Dot(-gee.normalized, gee * Time.fixedDeltaTime); + if (BDArmorySettings.DEBUG_OTHER) verticalSpeedAdjustment = Vector3.Dot(-gee.normalized, gee * Time.fixedDeltaTime); } verticalSpeed = Vector3.Dot(-gee.normalized, velocity); position += velocity * Time.fixedDeltaTime; - if (!FloatingOrigin.Offset.IsZero() || !Krakensbane.GetFrameVelocity().IsZero()) + if (BDKrakensbane.IsActive) { - position -= FloatingOrigin.OffsetNonKrakensbane; + position -= BDKrakensbane.FloatingOriginOffsetNonKrakensbane; } kerbal.vessel.IgnoreGForces(1); kerbal.vessel.IgnoreSpeed(1); @@ -203,18 +207,18 @@ IEnumerator ManuallyMoveKerbalEVACoroutine(KerbalEVA kerbal, Vector3 velocity, f kerbal.vessel.SetWorldVelocity(velocity); yield return wait; if (activeVesselBeforeEject != null && activeVesselBeforeEject != FlightGlobals.ActiveVessel) { LoadedVesselSwitcher.Instance.ForceSwitchVessel(activeVesselBeforeEject); } - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.KerbalSafety]: Setting " + kerbal.vessel.vesselName + "'s position to " + position.ToString("0.00") + " (" + kerbal.vessel.GetWorldPos3D().ToString("0.00") + ", altitude: " + kerbal.vessel.radarAltitude.ToString("0.00") + ") and velocity to " + velocity.magnitude.ToString("0.00") + " (" + kerbal.vessel.Velocity().magnitude.ToString("0.00") + ", " + verticalSpeed.ToString("0.00") + "m/s vertically, adjusted by " + verticalSpeedAdjustment.ToString("0.00") + "m/s)." + " (offset: " + !FloatingOrigin.Offset.IsZero() + ", frameVel: " + !Krakensbane.GetFrameVelocity().IsZero() + ")" + " " + Krakensbane.GetFrameVelocityV3f().ToString("0.0") + ", corr: " + Krakensbane.GetLastCorrection().ToString("0.0")); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log("[BDArmory.KerbalSafety]: Setting " + kerbal.vessel.vesselName + "'s position to " + position.ToString("0.00") + " (" + kerbal.vessel.GetWorldPos3D().ToString("0.00") + ", altitude: " + kerbal.vessel.radarAltitude.ToString("0.00") + ") and velocity to " + velocity.magnitude.ToString("0.00") + " (" + kerbal.vessel.Velocity().magnitude.ToString("0.00") + ", " + verticalSpeed.ToString("0.00") + "m/s vertically, adjusted by " + verticalSpeedAdjustment.ToString("0.00") + "m/s)." + " (offset: " + !BDKrakensbane.FloatingOriginOffset.IsZero() + ", frameVel: " + !Krakensbane.GetFrameVelocity().IsZero() + ")" + " " + BDKrakensbane.FrameVelocityV3f.ToString("0.0") + ", corr: " + Krakensbane.GetLastCorrection().ToString("0.0")); } if (kerbal != null && kerbal.vessel != null) { kerbal.vessel.rootPart.SetDetectCollisions(true); } - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_OTHER) { for (int count = 0; kerbal != null && kerbal.isActiveAndEnabled && kerbal.vessel != null && kerbal.vessel.isActiveAndEnabled && count < 10; ++count) { yield return wait; - Debug.Log("[BDArmory.KerbalSafety]: Tracking " + kerbal.vessel.vesselName + "'s position to " + kerbal.vessel.GetWorldPos3D().ToString("0.00") + " (altitude: " + kerbal.vessel.radarAltitude.ToString("0.00") + ") and velocity to " + kerbal.vessel.Velocity().magnitude.ToString("0.00") + " (" + kerbal.vessel.verticalSpeed.ToString("0.00") + "m/s vertically." + " (offset: " + !FloatingOrigin.Offset.IsZero() + ", frameVel: " + !Krakensbane.GetFrameVelocity().IsZero() + ")" + " " + Krakensbane.GetFrameVelocityV3f().ToString("0.0") + ", corr: " + Krakensbane.GetLastCorrection().ToString("0.0")); + Debug.Log("[BDArmory.KerbalSafety]: Tracking " + kerbal.vessel.vesselName + "'s position to " + kerbal.vessel.GetWorldPos3D().ToString("0.00") + " (altitude: " + kerbal.vessel.radarAltitude.ToString("0.00") + ") and velocity to " + kerbal.vessel.Velocity().magnitude.ToString("0.00") + " (" + kerbal.vessel.verticalSpeed.ToString("0.00") + "m/s vertically." + " (offset: " + !BDKrakensbane.FloatingOriginOffset.IsZero() + ", frameVel: " + !Krakensbane.GetFrameVelocity().IsZero() + ")" + " " + BDKrakensbane.FrameVelocityV3f.ToString("0.0") + ", corr: " + Krakensbane.GetLastCorrection().ToString("0.0")); } } } @@ -232,14 +236,24 @@ public void RecoverVesselNow(Vessel vessel) if (kerbals.ContainsKey(crew.displayName)) { kerbals[crew.displayName].recovered = true; - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_OTHER) Debug.Log("[BDArmory.KerbalSafety]: Recovering " + kerbals[crew.displayName].kerbalName + "."); kerbals[crew.displayName].RemoveHandlers(); } } } if (vessel.protoVessel != null) - { ShipConstruction.RecoverVesselFromFlight(vessel.protoVessel, HighLogic.CurrentGame.flightState, true); } + { + try + { + foreach (var part in vessel.Parts.ToList()) part.OnJustAboutToBeDestroyed?.Invoke(); // Invoke any OnJustAboutToBeDestroyed events since RecoverVesselFromFlight calls DestroyImmediate, skipping the FX detachment triggers. + ShipConstruction.RecoverVesselFromFlight(vessel.protoVessel, HighLogic.CurrentGame.flightState, true); + } + catch (Exception e) + { + Debug.LogError($"[BDArmory.KerbalSafety]: Exception thrown while removing vessel: {e.Message}"); + } + } } void EatenByTheKraken(GameEvents.HostedFromToAction fromTo) @@ -257,7 +271,7 @@ void EatenByTheKraken(GameEvents.HostedFromToAction fromT } else { - if (fromTo.host != null) + if (fromTo.host != null && fromTo.host.loaded) { Debug.LogWarning("[BDArmory.KerbalSafety]: " + fromTo.host + " got eaten by the Kraken!"); fromTo.host.gameObject.SetActive(false); @@ -335,14 +349,16 @@ public IEnumerator Configure(ProtoCrewMember c, Part p) } var wait = new WaitForFixedUpdate(); while (p.vessel != null && (!p.vessel.loaded || p.vessel.packed)) yield return wait; // Wait for the vessel to be loaded. (Avoids kerbals not being registered in seats.) - if (p.vessel == null) + if (p.vessel == null || c == null) { + Debug.LogWarning($"[BDArmory.KerbalSafety]: Vessel or crew is null."); Destroy(this); yield break; } kerbalName = c.displayName; if (KerbalSafetyManager.Instance.kerbals.ContainsKey(kerbalName)) // Already managed { + Debug.LogWarning($"[BDArmory.KerbalSafety]: {kerbalName} is already being managed!"); Destroy(this); yield break; } @@ -356,12 +372,10 @@ public IEnumerator Configure(ProtoCrewMember c, Part p) crew.ResetInventory(false); // Reset the inventory to just a chute. break; } - if (p.isKerbalSeat()) // We were given the seat instead of the EVA kerbal. - { p = p.GetComponent().Occupant; } part = p; if (p.IsKerbalEVA()) { - this.kerbalEVA = p.GetComponent(); + kerbalEVA = p.GetComponent(); if (kerbalEVA.IsSeated()) { bool found = false; @@ -371,6 +385,7 @@ public IEnumerator Configure(ProtoCrewMember c, Part p) { seat = s; found = true; + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.KerbalSafety]: {kerbalName} in part {part} of {part.vessel.vesselName} found in seat {seat}"); break; } } @@ -384,6 +399,7 @@ public IEnumerator Configure(ProtoCrewMember c, Part p) } else // Free-falling EVA kerbal. { + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.KerbalSafety]: Found a free-falling kerbal {kerbalName}."); ejected = true; StartCoroutine(DelayedChuteDeployment()); StartCoroutine(RecoverWhenPossible()); @@ -392,22 +408,29 @@ public IEnumerator Configure(ProtoCrewMember c, Part p) } AddHandlers(); KerbalSafetyManager.Instance.kerbals.Add(kerbalName, this); - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_OTHER) Debug.Log("[BDArmory.KerbalSafety]: Managing the safety of " + kerbalName + (ejected ? " on EVA" : " in " + p.vessel.vesselName) + "."); OnVesselModified(p.vessel); // Immediately check the vessel. } private void ConfigureKerbalEVA(KerbalEVA kerbalEVA) { + if ((Versioning.version_major == 1 && Versioning.version_minor > 10) || Versioning.version_major > 1) // Introduced in 1.11 + ConfigureKerbalEVA_1_11(kerbalEVA); chute = VesselModuleRegistry.GetModule(kerbalEVA.vessel); if (chute != null) - chute.deploymentState = ModuleEvaChute.deploymentStates.STOWED; // Make sure the chute is stowed. - if ((Versioning.version_major == 1 && Versioning.version_minor > 10) || Versioning.version_major > 1) // Introduced in 1.11 { - DisableConstructionMode(kerbalEVA); - if (BDArmorySettings.KERBAL_SAFETY_INVENTORY > 0) kerbalEVA.ModuleInventoryPartReference.SetInventoryDefaults(); - if (BDArmorySettings.KERBAL_SAFETY_INVENTORY == 2) RemoveJetpack(kerbalEVA); + chute.deploymentState = ModuleEvaChute.deploymentStates.STOWED; // Make sure the chute is stowed. + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.KerbalSafety]: Stowing parachute on {kerbalName}."); } + else if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.KerbalSafety]: No parachute found on {kerbalName}."); + } + + void ConfigureKerbalEVA_1_11(KerbalEVA kerbalEVA) + { + DisableConstructionMode(kerbalEVA); + if (BDArmorySettings.KERBAL_SAFETY_INVENTORY > 0) kerbalEVA.ModuleInventoryPartReference.SetInventoryDefaults(); + if (BDArmorySettings.KERBAL_SAFETY_INVENTORY == 2) RemoveJetpack(kerbalEVA); } public void ReconfigureInventory() @@ -428,17 +451,26 @@ private void RemoveJetpack(KerbalEVA kerbalEVA) var inventory = kerbalEVA.ModuleInventoryPartReference; if (inventory.ContainsPart("evaJetpack")) { + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.KerbalSafety]: Removing jetpack from {kerbalName}."); inventory.RemoveNPartsFromInventory("evaJetpack", 1, false); } kerbalEVA.part.UpdateMass(); } + // void OnDisable() // Find out who's destroying us. + // { + // if (gameObject.activeInHierarchy) + // { + // Debug.LogError($"DEBUG KerbalSafety {kerbalName} is being destroyed!"); + // } + // } + public void OnDestroy() { StopAllCoroutines(); - if (KerbalSafetyManager.Instance.safetyLevel != KerbalSafetyLevel.Off && !recovered && BDArmorySettings.DRAW_DEBUG_LABELS) + if (KerbalSafetyManager.Instance.safetyLevel != KerbalSafetyLevel.Off && !recovered && BDArmorySettings.DEBUG_OTHER) { - Debug.Log("[BDArmory.KerbalSafety]: " + kerbalName + " is MIA. Ejected: " + ejected + ", deployed chute: " + deployingChute); + Debug.Log($"[BDArmory.KerbalSafety]: {kerbalName} is MIA. Ejected: {ejected}, deployed chute: {deployingChute}."); } if (kerbalName != null && KerbalSafetyManager.Instance.kerbals.ContainsKey(kerbalName)) { @@ -495,22 +527,19 @@ public void Eject() { if (ejected) return; // We've already ejected. if (part == null || part.vessel == null) return; // The vessel is gone, don't try to do anything. - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log("[BDArmory.KerbalSafety]: Ejection triggered for " + kerbalName + " in " + part); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.KerbalSafety]: Ejection triggered for {kerbalName} in {part}."); if (kerbalEVA != null) { if (kerbalEVA.isActiveAndEnabled) // Otherwise, they've been killed already and are being cleaned up by KSP. { if (seat != null && kerbalEVA.IsSeated()) // Leave the seat. { - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log("[BDArmory.KerbalSafety]: " + kerbalName + " is leaving their seat on " + seat.part.vessel.vesselName + "."); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.KerbalSafety]: {kerbalName} is leaving their seat on {seat.part.vessel.vesselName}."); seat.LeaveSeat(new KSPActionParam(KSPActionGroup.Abort, KSPActionType.Activate)); } else { - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log("[BDArmory.KerbalSafety]: " + kerbalName + " has already left their seat."); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.KerbalSafety]: {kerbalName} has already left their seat."); } StartCoroutine(DelayedChuteDeployment()); StartCoroutine(RecoverWhenPossible()); @@ -525,7 +554,7 @@ public void Eject() // { message = kerbalName + " failed to eject from " + part.vessel.vesselName + ", all exits were blocked. R.I.P."; // BDACompetitionMode.Instance.competitionStatus.Add(message); - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_OTHER) Debug.Log("[BDArmory.KerbalSafety]: " + message); // } } @@ -548,8 +577,7 @@ private bool EjectFromOtherPart() var crewTransfer = CrewTransfer.Create(fromPart, crew, OnDialogDismiss); if (crewTransfer != null && crewTransfer.validParts.Contains(toPart)) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log("[BDArmory.KerbalSafety]: Transferring " + kerbalName + " from " + fromPart + " to " + toPart + " then ejecting."); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.KerbalSafety]: Transferring {kerbalName} from {fromPart} to {toPart} then ejecting."); crewTransfer.MoveCrewTo(toPart); if (ProcessEjection(toPart)) return true; @@ -562,8 +590,8 @@ private bool EjectFromOtherPart() private void OnDialogDismiss(PartItemTransfer.DismissAction arg1, Part arg2) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log(arg1); - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log(arg2); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log(arg1); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log(arg2); } private bool ProcessEjection(Part fromPart) @@ -578,7 +606,7 @@ private bool ProcessEjection(Part fromPart) crew.KerbalRef.state = Kerbal.States.BAILED_OUT; fromPart.vessel.RemoveCrew(crew); } - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_OTHER) Debug.Log("[BDArmory.KerbalSafety]: " + kerbalName + " ejected from " + fromPart.vessel.vesselName + " at " + fromPart.vessel.radarAltitude.ToString("0.00") + "m with velocity " + fromPart.vessel.Velocity().magnitude.ToString("0.00") + "m/s (vertical: " + fromPart.vessel.verticalSpeed + $")"); kerbalEVA.autoGrabLadderOnStart = false; // Don't grab the vessel. kerbalEVA.StartNonCollidePeriod(5f, 1f, fromPart, fromPart.airlock); @@ -607,31 +635,33 @@ public void OnVesselModified(Vessel vessel) { if (kerbalEVA.isActiveAndEnabled) { - if (vessel.parts.Count == 1) // It's a falling kerbal. + switch (vessel.parts.Count) { - if (!ejected) - { - ejected = true; - StartCoroutine(DelayedChuteDeployment()); - StartCoroutine(RecoverWhenPossible()); - } - } - else // It's a kerbal in a seat. - { - // Check if the kerbal needs to leave his seat. - ejected = false; - if (vessel.parts.Count == 2) // Just a kerbal in a seat. - { - StartCoroutine(DelayedLeaveSeat()); - } - else { } // FIXME What else? + case 0: // He's dead, Jim. + Debug.Log($"[BDArmory.KerbalSafety]: {kerbalName} was killed!"); + break; + case 1: // It's a falling kerbal. + if (!ejected) + { + ejected = true; + StartCoroutine(DelayedChuteDeployment()); + StartCoroutine(RecoverWhenPossible()); + } + break; + default: // It's a kerbal in a seat. + ejected = false; + if (vessel.parts.Count == 2) // Just a kerbal in a seat. + { + StartCoroutine(DelayedLeaveSeat()); + } + else { } // FIXME What else? + break; } } else { - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log("[BDArmory.KerbalSafety]: " + kerbalName + " was not active (probably dead and being cleaned up by KSP already)."); - OnDestroy(); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.KerbalSafety]: {kerbalName} was not active (probably dead and being cleaned up by KSP already)."); + Destroy(this); } } else // It's a crew. @@ -652,23 +682,25 @@ IEnumerator DelayedChuteDeployment(float delay = 1f) { yield break; } + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.KerbalSafety]: Deploying chute on {kerbalName} in {delay}s"); deployingChute = true; // Indicate that we're deploying our chute. ejected = true; // Also indicate that we've ejected. - yield return new WaitForSeconds(delay); + yield return new WaitForSecondsFixed(delay); if (kerbalEVA == null) yield break; kerbalEVA.vessel.altimeterDisplayState = AltimeterDisplayState.AGL; if (chute != null && !kerbalEVA.IsSeated() && !kerbalEVA.vessel.LandedOrSplashed) // Check that the kerbal hasn't regained their seat or already landed. { - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log("[BDArmory.KerbalSafety]: " + kerbalName + " is falling, deploying halo parachute at " + kerbalEVA.vessel.radarAltitude + "m."); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.KerbalSafety]: {kerbalName} is falling, deploying halo parachute at {kerbalEVA.vessel.radarAltitude}m."); if (chute.deploymentState != ModuleParachute.deploymentStates.SEMIDEPLOYED) chute.deploymentState = ModuleParachute.deploymentStates.STOWED; // Reset the deployment state. chute.deployAltitude = 30f; chute.Deploy(); } - if (kerbalEVA.vessel.LandedOrSplashed) - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log("[BDArmory.KerbalSafety]: " + kerbalEVA.vessel.vesselName + " has already landed, not deploying chute."); + else + { + deployingChute = false; + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.KerbalSafety]: Not deploying {kerbalName}'s chute due to {(chute == null ? "chute is null" : "")}{(kerbalEVA.IsSeated() ? "still being seated" : "")}{(kerbalEVA.vessel.LandedOrSplashed ? "having already landed/splashed" : "")}."); + } if (FlightGlobals.ActiveVessel == kerbalEVA.vessel) LoadedVesselSwitcher.Instance.TriggerSwitchVessel(1f); } @@ -684,11 +716,11 @@ IEnumerator DelayedLeaveSeat(float delay = 3f) yield break; } leavingSeat = true; - yield return new WaitForSeconds(delay); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.KerbalSafety]: {kerbalName} is leaving seat in {delay}s."); + yield return new WaitForSecondsFixed(delay); if (seat != null) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log("[BDArmory.KerbalSafety]: Found " + kerbalName + " in a combat chair just falling, ejecting."); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.KerbalSafety]: Found {kerbalName} in a combat chair just falling, ejecting."); seat.LeaveSeat(new KSPActionParam(KSPActionGroup.Abort, KSPActionType.Activate)); ejected = true; StartCoroutine(DelayedChuteDeployment()); @@ -714,20 +746,28 @@ public IEnumerator RecoverWhenPossible(bool asap = false) recovering = true; if (!asap) { - yield return new WaitUntil(() => kerbalEVA == null || kerbalEVA.vessel.LandedOrSplashed); - yield return new WaitForSeconds(5); // Give it around 5s after landing, then recover the kerbal + yield return new WaitUntilFixed(() => kerbalEVA == null || kerbalEVA.vessel.LandedOrSplashed); + yield return new WaitForSecondsFixed(5); // Give it around 5s after landing, then recover the kerbal } - yield return new WaitUntil(() => kerbalEVA == null || FlightGlobals.ActiveVessel != kerbalEVA.vessel); + yield return new WaitUntilFixed(() => kerbalEVA == null || FlightGlobals.ActiveVessel != kerbalEVA.vessel); + if (KerbalSafetyManager.Instance.kerbals.ContainsKey(kerbalName)) + KerbalSafetyManager.Instance.kerbals.Remove(kerbalName); // Stop managing this kerbal. if (kerbalEVA == null) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.LogError("[BDArmory.KerbalSafety]: " + kerbalName + " on EVA is MIA."); + if (BDArmorySettings.DEBUG_OTHER) Debug.LogError($"[BDArmory.KerbalSafety]: {kerbalName} on EVA is MIA."); yield break; } - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log("[BDArmory.KerbalSafety]: Recovering " + kerbalName + "."); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.KerbalSafety]: Recovering {kerbalName}."); recovered = true; - ShipConstruction.RecoverVesselFromFlight(kerbalEVA.vessel.protoVessel, HighLogic.CurrentGame.flightState, true); + try + { + foreach (var part in kerbalEVA.vessel.Parts) part.OnJustAboutToBeDestroyed?.Invoke(); // Invoke any OnJustAboutToBeDestroyed events since RecoverVesselFromFlight calls DestroyImmediate, skipping the FX detachment triggers. + ShipConstruction.RecoverVesselFromFlight(kerbalEVA.vessel.protoVessel, HighLogic.CurrentGame.flightState, true); + } + catch (Exception e) + { + Debug.LogError($"[BDArmory.KerbalSafety]: Exception thrown while removing vessel: {e.Message}"); + } } #endregion } diff --git a/BDArmory/Modules/ModuleMissileRearm.cs b/BDArmory/Modules/ModuleMissileRearm.cs deleted file mode 100644 index 7a31034bf..000000000 --- a/BDArmory/Modules/ModuleMissileRearm.cs +++ /dev/null @@ -1,546 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using KSP.UI.Screens; -using UnityEngine; - -namespace BDArmory.Modules -{ - public class ModuleMissileRearm : PartModule - { - private Transform MissileTransform = null; - - [KSPField(guiName = "#LOC_BDArmory_OrdinanceAvailable", guiActive = true, isPersistant = true)]//Ordinance Available - public int ammoCount = 20; - - [KSPField(guiName = "#LOC_BDArmory_MissileAssign", guiActive = true, isPersistant = true)]//Missile Assign - private string MissileName = "bahaAim120"; - - [KSPAction("Resupply", KSPActionGroup.None)] - private void ActionResupply(KSPActionParam param) - { - Resupply(); - } - - [KSPEvent(name = "Resupply", guiName = "#LOC_BDArmory_Resupply", active = true, guiActive = true)]//Resupply - public void Resupply() - { - if (this.part.children.Count != 0) - { - Debug.Log("[BDArmory.ModuleMissileRearm]: Not Empty" + this.part.children.Count); - return; - } - if (ammoCount >= 1) - { - List availablePart = PartLoader.LoadedPartsList; - foreach (AvailablePart AP in availablePart) - { - if (AP.partPrefab.name == MissileName) - { - foreach (PartModule m in AP.partPrefab.Modules) - { - if (m.moduleName == "MissileLauncher") - { - var partNode = new ConfigNode(); - PartSnapshot(AP.partPrefab).CopyTo(partNode); - Debug.Log("[BDArmory.ModuleMissileRearm]: Node" + AP.partPrefab.srfAttachNode.originalPosition); - CreatePart(partNode, MissileTransform.transform.position - MissileTransform.TransformDirection(AP.partPrefab.srfAttachNode.originalPosition), - this.part.transform.rotation, this.part, this.part, "srfAttach"); - ammoCount -= 1; - StartCoroutine(ResetTurret()); - return; - } - } - } - } - } - } - - IEnumerator ResetTurret() - { - yield return new WaitForEndOfFrame(); - yield return new WaitForEndOfFrame(); - - var turret = part.FindModuleImplementing(); - if (turret != null) - { - turret.UpdateMissileChildren(); - } - } - - //[KSPEvent(name = "Reassign", guiName = "Reassign", active = true, guiActive = true)] - public void Reassign() - { - if (this.part.children.Count == 1) - { - foreach (Part p in this.part.children) - { - foreach (PartModule m in p.Modules) - { - if (m.moduleName == "MissileLauncher") - { - MissileName = p.name; - Debug.Log("[BDArmory.ModuleMissileRearm]: " + MissileName); - } - } - } - } - } - - public override void OnStart(PartModule.StartState state) - { - this.enabled = true; - this.part.force_activate(); - MissileTransform = base.part.FindModelTransform("MissileTransform"); - Reassign(); - } - - public override void OnFixedUpdate() - { - } - - [KSPEvent(name = "Resupply", guiName = "#LOC_BDArmory_Resupply", active = true, guiActive = false)]//Resupply - public static AttachNode GetAttachNodeById(Part p, string id) - { - var node = id == "srfAttach" ? p.srfAttachNode : p.FindAttachNode(id); - if (node == null) - { - Debug.Log("[BDArmory.ModuleMissileRearm]: Cannot find attach node {0} on part {1}. Using srfAttach" + id + p); - node = p.srfAttachNode; - } - return node; - } - - public static ModuleDockingNode GetDockingNode( - Part part, string attachNodeId = null, AttachNode attachNode = null) - { - var nodeId = attachNodeId ?? (attachNode != null ? attachNode.id : null); - return part.FindModulesImplementing() - .FirstOrDefault(x => x.referenceAttachNode == nodeId); - } - - public static bool CoupleDockingPortWithPart(ModuleDockingNode dockingNode) - { - var tgtPart = dockingNode.referenceNode.attachedPart; - if (tgtPart == null) - { - Debug.Log("[BDArmory.ModuleMissileRearm]: Node's part {0} is not attached to anything thru the reference node" + dockingNode.part); - return false; - } - if (dockingNode.state != dockingNode.st_ready.name) - { - Debug.Log("[BDArmory.ModuleMissileRearm]: Hard reset docking node {0} from state '{1}' to '{2}'" + - dockingNode.part + dockingNode.state + dockingNode.st_ready.name); - dockingNode.dockedPartUId = 0; - dockingNode.dockingNodeModuleIndex = 0; - // Target part lived in real world for some time, so its state may be anything. - // Do a hard reset. - dockingNode.fsm.StartFSM(dockingNode.st_ready.name); - } - var initState = dockingNode.lateFSMStart(PartModule.StartState.None); - // Make sure part init catched the new state. - while (initState.MoveNext()) - { - // Do nothing. Just wait. - } - if (dockingNode.fsm.currentStateName != dockingNode.st_preattached.name) - { - Debug.Log("[BDArmory.ModuleMissileRearm]: Node on {0} is unexpected state '{1}'" + - dockingNode.part + dockingNode.fsm.currentStateName); - return false; - } - Debug.Log("[BDArmory.ModuleMissileRearm]: Successfully set docking node {0} to state {1} with part {2}" + - dockingNode.part + dockingNode.fsm.currentStateName + tgtPart); - return true; - } - - static IEnumerator WaitAndMakeLonePart(Part newPart, OnPartReady onPartReady) - { - Debug.Log("[BDArmory.ModuleMissileRearm]: Create lone part vessel for {0}" + newPart); - string originatingVesselName = newPart.vessel.vesselName; - newPart.physicalSignificance = Part.PhysicalSignificance.NONE; - newPart.PromoteToPhysicalPart(); - newPart.Unpack(); - newPart.disconnect(true); - Vessel newVessel = newPart.gameObject.AddComponent(); - newVessel.id = Guid.NewGuid(); - if (newVessel.Initialize(false)) - { - newVessel.vesselName = Vessel.AutoRename(newVessel, originatingVesselName); - newVessel.IgnoreGForces(10); - newVessel.currentStage = StageManager.RecalculateVesselStaging(newVessel); - newPart.setParent(null); - } - yield return new WaitWhile(() => !newPart.started && newPart.State != PartStates.DEAD); - Debug.Log("[BDArmory.ModuleMissileRearm]: Part {0} is in state {1}" + newPart + newPart.State); - if (newPart.State == PartStates.DEAD) - { - Debug.Log("[BDArmory.ModuleMissileRearm]: Part {0} has died before fully instantiating" + newPart); - yield break; - } - - if (onPartReady != null) - { - onPartReady(newPart); - } - } - - public static void AwakePartModule(PartModule module) - { - // Private method can only be accessed via reflection when requested on the class that declares - // it. So, don't use type of the argument and specify it explicitly. - var moduleAwakeMethod = typeof(PartModule).GetMethod( - "Awake", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); - if (moduleAwakeMethod != null) - { - moduleAwakeMethod.Invoke(module, new object[] { }); - } - else - { - Debug.Log("[BDArmory.ModuleMissileRearm]: Cannot find Awake() method on {0}. Skip awakening", module); - } - } - - public static void ResetPartModule(PartModule module) - { - // Private method can only be accessed via reflection when requested on the class that declares - // it. So, don't use type of the argument and specify it explicitly. - var moduleResetMethod = typeof(PartModule).GetMethod( - "UpdateMissileChildren", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); - if (moduleResetMethod != null) - { - moduleResetMethod.Invoke(module, new object[] { }); - } - else - { - Debug.Log("[BDArmory.ModuleMissileRearm]: Cannot find Awake() method on {0}. Skip awakening", module); - } - } - - public static void CleanupFieldsInModule(PartModule module) - { - // HACK: Fix uninitialized fields in science lab module. - var scienceModule = module as ModuleScienceLab; - if (scienceModule != null) - { - scienceModule.ExperimentData = new List(); - Debug.Log("[BDArmory.ModuleMissileRearm]: WORKAROUND. Fix null field in ModuleScienceLab module on the part prefab: {0}", module); - } - - // Ensure the module is awaken. Otherwise, any access to base fields list will result in NRE. - // HACK: Accessing Fields property of a non-awaken module triggers NRE. If it happens then do - // explicit awakening of the *base* module class. - try - { - using (var field = module.Fields.GetEnumerator()) { }; - } - catch (Exception e) - { - Debug.Log("[BDArmory.ModuleMissileRearm]: WORKAROUND. Module " + module + " on part prefab is not awaken. Call Awake on it. Exception: " + e.Message); - AwakePartModule(module); - } - foreach (var field in module.Fields) - { - var baseField = field as BaseField; - if (baseField.isPersistant && baseField.GetValue(module) == null) - { - //var proto = new StandardOrdinaryTypesProto(); - //var defValue = proto.ParseFromString("", baseField.FieldInfo.FieldType); - //Debug.Log("[BDArmory.ModuleMissileRearm]: WORKAROUND. Found null field {0} in module prefab {1}," - // + " fixing to default value of type {2}: {3}", - // baseField.name, module, baseField.FieldInfo.FieldType, defValue); - //baseField.SetValue(defValue, module); - } - } - } - - public static void CleanupModuleFieldsInPart(Part part) - { - var badModules = new List(); - foreach (var moduleObj in part.Modules) - { - var module = moduleObj as PartModule; - try - { - CleanupFieldsInModule(module); - } - catch (Exception e) - { - badModules.Add(module); - Debug.LogWarning("[BDArmory.ModuleMissileRearm]: Exception thrown in CleanupModuleFieldsInPart: " + e.Message + "\n" + e.StackTrace); - } - } - // Cleanup modules that block KIS. It's a bad thing to do but not working KIS is worse. - foreach (var moduleToDrop in badModules) - { - Debug.Log("[BDArmory.ModuleMissileRearm]: Module on part prefab {0} is setup improperly: name={1}. Drop it!" + part, moduleToDrop); - part.RemoveModule(moduleToDrop); - } - } - - public static ConfigNode PartSnapshot(Part part) - { - if (ReferenceEquals(part, part.partInfo.partPrefab)) - { - // HACK: Prefab may have fields initialized to "null". Such fields cannot be saved via - // BaseFieldList when making a snapshot. So, go thru the persistent fields of all prefab - // modules and replace nulls with a default value of the type. It's unlikely we break - // something since by design such fields are not assumed to be used until loaded, and it's - // impossible to have "null" value read from a config. - CleanupModuleFieldsInPart(part); - } - - var node = new ConfigNode("PART"); - var snapshot = new ProtoPartSnapshot(part, null); - - snapshot.attachNodes = new List(); - snapshot.srfAttachNode = new AttachNodeSnapshot("attach,-1"); - snapshot.symLinks = new List(); - snapshot.symLinkIdxs = new List(); - snapshot.Save(node); - - // Prune unimportant data - node.RemoveValues("parent"); - node.RemoveValues("position"); - node.RemoveValues("rotation"); - node.RemoveValues("istg"); - node.RemoveValues("dstg"); - node.RemoveValues("sqor"); - node.RemoveValues("sidx"); - node.RemoveValues("attm"); - node.RemoveValues("srfN"); - node.RemoveValues("attN"); - node.RemoveValues("connected"); - node.RemoveValues("attached"); - node.RemoveValues("flag"); - - node.RemoveNodes("ACTIONS"); - - // Remove modules that are not in prefab since they won't load anyway - var module_nodes = node.GetNodes("MODULE"); - var prefab_modules = part.partInfo.partPrefab.GetComponents(); - node.RemoveNodes("MODULE"); - - for (int i = 0; i < prefab_modules.Length && i < module_nodes.Length; i++) - { - var module = module_nodes[i]; - var name = module.GetValue("name") ?? ""; - - node.AddNode(module); - - if (name == "KASModuleContainer") - { - // Containers get to keep their contents - module.RemoveNodes("EVENTS"); - } - else if (name.StartsWith("KASModule")) - { - // Prune the state of the KAS modules completely - module.ClearData(); - module.AddValue("name", name); - continue; - } - - module.RemoveNodes("ACTIONS"); - } - - return node; - } - - public delegate void OnPartReady(Part affectedPart); - - public static Part CreatePart(AvailablePart avPart, Vector3 position, Quaternion rotation, - Part fromPart) - { - var partNode = new ConfigNode(); - PartSnapshot(avPart.partPrefab).CopyTo(partNode); - return CreatePart(partNode, position, rotation, fromPart); - } - - /// Creates a new part from the config. - /// Config to read part from. - /// Initial position of the new part. - /// Initial rotation of the new part. - /// - /// Optional. Part to couple new part to. - /// - /// Optional. Attach node ID on the new part to use for coupling. It's required if coupling to - /// part is requested. - /// - /// - /// Optional. Attach node on the target part to use for coupling. It's required if - /// specifies a stack node. - /// - /// - /// Callback to call when new part is fully operational and its joint is created (if any). It's - /// undetermined how long it may take before the callback is called. The calling code must expect - /// that there will be several frame updates and at least one fixed frame update. - /// - /// - /// Tells if new part must be created without rigidbody and joint. It's only used to create - /// equippable parts. Any other use-case is highly unlikely. - /// - /// - public static Part CreatePart( - ConfigNode partConfig, - Vector3 position, - Quaternion rotation, - Part fromPart, - Part coupleToPart = null, - string srcAttachNodeId = null, - AttachNode tgtAttachNode = null, - OnPartReady onPartReady = null, - bool createPhysicsless = false) - { - // Sanity checks for the parameters. - if (coupleToPart != null) - { - if (srcAttachNodeId == null - || srcAttachNodeId == "srfAttach" && tgtAttachNode != null - || srcAttachNodeId != "srfAttach" - && (tgtAttachNode == null || tgtAttachNode.id == "srfAttach")) - { - // Best we can do is falling back to surface attach. - srcAttachNodeId = "srfAttach"; - tgtAttachNode = null; - } - } - - var refVessel = coupleToPart != null ? coupleToPart.vessel : fromPart.vessel; - var partNodeCopy = new ConfigNode(); - partConfig.CopyTo(partNodeCopy); - var snapshot = - new ProtoPartSnapshot(partNodeCopy, refVessel.protoVessel, HighLogic.CurrentGame); - if (HighLogic.CurrentGame.flightState.ContainsFlightID(snapshot.flightID) - || snapshot.flightID == 0) - { - snapshot.flightID = ShipConstruction.GetUniqueFlightID(HighLogic.CurrentGame.flightState); - } - snapshot.parentIdx = coupleToPart != null ? refVessel.parts.IndexOf(coupleToPart) : 0; - snapshot.position = position; - snapshot.rotation = rotation; - snapshot.stageIndex = 0; - snapshot.defaultInverseStage = 0; - snapshot.seqOverride = -1; - snapshot.inStageIndex = -1; - snapshot.attachMode = srcAttachNodeId == "srfAttach" - ? (int)AttachModes.SRF_ATTACH - : (int)AttachModes.STACK; - snapshot.attached = true; - snapshot.flagURL = fromPart.flagURL; - - var newPart = snapshot.Load(refVessel, false); - refVessel.Parts.Add(newPart); - newPart.transform.position = position; - newPart.transform.rotation = rotation; - newPart.missionID = fromPart.missionID; - newPart.UpdateOrgPosAndRot(newPart.vessel.rootPart); - - if (coupleToPart != null) - { - // Wait for part to initialize and then fire ready event. - Debug.Log("[BDArmory.ModuleMissileRearm]: Ready to error" + newPart + srcAttachNodeId + tgtAttachNode); - newPart.StartCoroutine( - WaitAndCouple(newPart, srcAttachNodeId, tgtAttachNode, onPartReady, - createPhysicsless: createPhysicsless)); - } - else - { - // Create new part as a separate vessel. - newPart.StartCoroutine(WaitAndMakeLonePart(newPart, onPartReady)); - } - return newPart; - } - - static IEnumerator WaitAndCouple(Part newPart, string srcAttachNodeId, - AttachNode tgtAttachNode, OnPartReady onPartReady, - bool createPhysicsless = false) - { - var tgtPart = newPart.parent; - if (createPhysicsless) - { - newPart.PhysicsSignificance = 1; // Disable physics on the part. - } - - // Create proper attach nodes. - Debug.Log("[BDArmory.ModuleMissileRearm]: Attach new part {0} to {1}: srcNodeId={2}, tgtNode={3}" + - newPart + newPart.vessel + - srcAttachNodeId); - var srcAttachNode = GetAttachNodeById(newPart, srcAttachNodeId); - srcAttachNode.attachedPart = tgtPart; - srcAttachNode.attachedPartId = tgtPart.flightID; - if (tgtAttachNode != null) - { - tgtAttachNode.attachedPart = newPart; - tgtAttachNode.attachedPartId = newPart.flightID; - } - - // When target, source or both are docking ports force them into state PreAttached. It's the - // most safe state that simulates behavior of parts attached in the editor. - var srcDockingNode = GetDockingNode(newPart, attachNodeId: srcAttachNodeId); - if (srcDockingNode != null) - { - // Source part is not yet started. It's functionality is very limited. - srcDockingNode.state = "PreAttached"; - srcDockingNode.dockedPartUId = 0; - srcDockingNode.dockingNodeModuleIndex = 0; - Debug.Log("[BDArmory.ModuleMissileRearm]: Force new node {0} to state {1}" + newPart + srcDockingNode.state); - } - var tgtDockingNode = GetDockingNode(tgtPart, attachNode: tgtAttachNode); - if (tgtDockingNode != null) - { - CoupleDockingPortWithPart(tgtDockingNode); - } - - // Wait until part is started. Keep it in position till it happen. - Debug.Log("[BDArmory.ModuleMissileRearm]: Wait for part {0} to get alive...", newPart); - newPart.transform.parent = tgtPart.transform; - var relPos = newPart.transform.localPosition; - var relRot = newPart.transform.localRotation; - if (newPart.PhysicsSignificance != 1) - { - // Mangling with colliders on physicsless parts may result in camera effects. - var childColliders = newPart.GetComponentsInChildren(includeInactive: false); - CollisionManager.IgnoreCollidersOnVessel(tgtPart.vessel, childColliders); - } - while (!newPart.started && newPart.State != PartStates.DEAD) - { - yield return new WaitForFixedUpdate(); - if (newPart.rb != null) - { - newPart.rb.position = newPart.parent.transform.TransformPoint(relPos); - newPart.rb.rotation = newPart.parent.transform.rotation * relRot; - newPart.rb.velocity = newPart.parent.Rigidbody.velocity; - newPart.rb.angularVelocity = newPart.parent.Rigidbody.angularVelocity; - } - } - newPart.transform.parent = newPart.transform; - Debug.Log("[BDArmory.ModuleMissileRearm]: Part {0} is in state {1}" + newPart + newPart.State); - if (newPart.State == PartStates.DEAD) - { - Debug.Log("[BDArmory.ModuleMissileRearm]: Part {0} has died before fully instantiating", newPart); - yield break; - } - - // Complete part initialization. - newPart.Unpack(); - newPart.InitializeModules(); - - // Notify game about a new part that has just "coupled". - GameEvents.onPartCouple.Fire(new GameEvents.FromToAction(newPart, tgtPart)); - tgtPart.vessel.ClearStaging(); - GameEvents.onVesselPartCountChanged.Fire(tgtPart.vessel); - newPart.vessel.checkLanded(); - newPart.vessel.currentStage = StageManager.RecalculateVesselStaging(tgtPart.vessel) + 1; - GameEvents.onVesselWasModified.Fire(tgtPart.vessel); - newPart.CheckBodyLiftAttachment(); - - if (onPartReady != null) - { - onPartReady(newPart); - } - } - } -} diff --git a/BDArmory/Modules/ModuleMovingPart.cs b/BDArmory/Modules/ModuleMovingPart.cs index 0d06b142b..9aa56854e 100644 --- a/BDArmory/Modules/ModuleMovingPart.cs +++ b/BDArmory/Modules/ModuleMovingPart.cs @@ -1,7 +1,9 @@ using System.Collections; -using BDArmory.UI; using UnityEngine; +using BDArmory.UI; +using BDArmory.Utils; + namespace BDArmory.Modules { public class ModuleMovingPart : PartModule @@ -42,10 +44,8 @@ void FixedUpdate() IEnumerator SetupRoutine() { - while (vessel.packed) - { - yield return null; - } + yield return new WaitWhile(() => vessel is not null && (vessel.packed || !vessel.loaded)); + yield return new WaitForFixedUpdate(); SetupJoints(); } @@ -86,7 +86,7 @@ void OnGUI() { for (int i = 0; i < localAnchors.Length; i++) { - BDGUIUtils.DrawTextureOnWorldPos(parentTransform.TransformPoint(localAnchors[i]), + GUIUtils.DrawTextureOnWorldPos(parentTransform.TransformPoint(localAnchors[i]), BDArmorySetup.Instance.greenDotTexture, new Vector2(6, 6), 0); } } diff --git a/BDArmory/Modules/ModuleSelfSealingTank.cs b/BDArmory/Modules/ModuleSelfSealingTank.cs index 71a7ba309..4b23f1d18 100644 --- a/BDArmory/Modules/ModuleSelfSealingTank.cs +++ b/BDArmory/Modules/ModuleSelfSealingTank.cs @@ -6,10 +6,10 @@ using System; using UnityEngine; -using BDArmory.Core; using BDArmory.FX; -using BDArmory.Misc; +using BDArmory.Settings; using BDArmory.UI; +using BDArmory.Utils; namespace BDArmory.Modules { @@ -30,7 +30,7 @@ public void ToggleTankOption() SSTank = !SSTank; if (!SSTank) { - Events["ToggleTankOption"].guiName = Localizer.Format("#LOC_BDArmory_SSTank_On");//"Enable self-sealing tank" + Events["ToggleTankOption"].guiName = StringUtils.Localize("#LOC_BDArmory_SSTank_On");//"Enable self-sealing tank" using (IEnumerator resource = part.Resources.GetEnumerator()) while (resource.MoveNext()) @@ -42,7 +42,7 @@ public void ToggleTankOption() } else { - Events["ToggleTankOption"].guiName = Localizer.Format("#LOC_BDArmory_SSTank_Off");//"Disable self-sealing tank" + Events["ToggleTankOption"].guiName = StringUtils.Localize("#LOC_BDArmory_SSTank_Off");//"Disable self-sealing tank" using (IEnumerator resource = part.Resources.GetEnumerator()) while (resource.MoveNext()) @@ -52,7 +52,7 @@ public void ToggleTankOption() resource.Current.amount = Math.Min(resource.Current.amount, resource.Current.maxAmount); } } - Utils.RefreshAssociatedWindows(part); + GUIUtils.RefreshAssociatedWindows(part); using (List.Enumerator pSym = part.symmetryCounterparts.GetEnumerator()) while (pSym.MoveNext()) { @@ -65,7 +65,7 @@ public void ToggleTankOption() if (!SSTank) { - tank.Events["ToggleTankOption"].guiName = Localizer.Format("#LOC_BDArmory_SSTank_On");//"Enable self-sealing tank" + tank.Events["ToggleTankOption"].guiName = StringUtils.Localize("#LOC_BDArmory_SSTank_On");//"Enable self-sealing tank" using (IEnumerator resource = pSym.Current.Resources.GetEnumerator()) while (resource.MoveNext()) @@ -77,7 +77,7 @@ public void ToggleTankOption() } else { - tank.Events["ToggleTankOption"].guiName = Localizer.Format("#LOC_BDArmory_SSTank_Off");//"Disable self-sealing tank" + tank.Events["ToggleTankOption"].guiName = StringUtils.Localize("#LOC_BDArmory_SSTank_Off");//"Disable self-sealing tank" using (IEnumerator resource = pSym.Current.Resources.GetEnumerator()) while (resource.MoveNext()) @@ -87,7 +87,7 @@ public void ToggleTankOption() resource.Current.amount = Math.Min(resource.Current.amount, resource.Current.maxAmount); } } - Utils.RefreshAssociatedWindows(pSym.Current); + GUIUtils.RefreshAssociatedWindows(pSym.Current); } } @@ -100,14 +100,14 @@ public void ToggleInertOption() InertTank = !InertTank; if (!InertTank) { - Events["ToggleInertOption"].guiName = Localizer.Format("#LOC_BDArmory_FIS_On");//"Enable self-sealing tank" + Events["ToggleInertOption"].guiName = StringUtils.Localize("#LOC_BDArmory_FIS_On");//"Enable self-sealing tank" FISmass = 0; Fields["FireBottles"].guiActiveEditor = true; Fields["FBRemaining"].guiActive = true; } else { - Events["ToggleInertOption"].guiName = Localizer.Format("#LOC_BDArmory_FIS_Off");//"Disable self-sealing tank" + Events["ToggleInertOption"].guiName = StringUtils.Localize("#LOC_BDArmory_FIS_Off");//"Disable self-sealing tank" FISmass = 0.15f; FireBottles = 0; FBSetup(null, null); @@ -115,7 +115,7 @@ public void ToggleInertOption() Fields["FBRemaining"].guiActive = false; } partmass = (FISmass + ArmorMass + FBmass); - Utils.RefreshAssociatedWindows(part); + GUIUtils.RefreshAssociatedWindows(part); using (List.Enumerator pSym = part.symmetryCounterparts.GetEnumerator()) while (pSym.MoveNext()) { @@ -128,20 +128,20 @@ public void ToggleInertOption() if (!InertTank) { - tank.Events["ToggleInertOption"].guiName = Localizer.Format("#LOC_BDArmory_FIS_On");//"Add Fuel Inerting System" + tank.Events["ToggleInertOption"].guiName = StringUtils.Localize("#LOC_BDArmory_FIS_On");//"Add Fuel Inerting System" tank.FISmass = 0; tank.Fields["FireBottles"].guiActiveEditor = true; tank.Fields["FBRemaining"].guiActive = true; } else { - tank.Events["ToggleInertOption"].guiName = Localizer.Format("#LOC_BDArmory_FIS_Off");//"Remove Fuel Inerting System" + tank.Events["ToggleInertOption"].guiName = StringUtils.Localize("#LOC_BDArmory_FIS_Off");//"Remove Fuel Inerting System" tank.FISmass = 0.15f; tank.Fields["FireBottles"].guiActiveEditor = false; tank.Fields["FBRemaining"].guiActive = false; } tank.partmass = (tank.FISmass + tank.ArmorMass + tank.FBmass); - Utils.RefreshAssociatedWindows(pSym.Current); + GUIUtils.RefreshAssociatedWindows(pSym.Current); } if (HighLogic.LoadedSceneIsEditor && EditorLogic.fetch != null) GameEvents.onEditorShipModified.Fire(EditorLogic.fetch.ship); @@ -156,16 +156,16 @@ public void TogglecockpitArmor() armoredCockpit = !armoredCockpit; if (!armoredCockpit) { - Events["TogglecockpitArmor"].guiName = Localizer.Format("#LOC_BDArmory_Armorcockpit_On");//"Add Armored Cockpit" + Events["TogglecockpitArmor"].guiName = StringUtils.Localize("#LOC_BDArmory_Armorcockpit_On");//"Add Armored Cockpit" ArmorMass = 0; } else { - Events["TogglecockpitArmor"].guiName = Localizer.Format("#LOC_BDArmory_Armorcockpit_Off");//"Remove Armored Cockpit" + Events["TogglecockpitArmor"].guiName = StringUtils.Localize("#LOC_BDArmory_Armorcockpit_Off");//"Remove Armored Cockpit" ArmorMass = 0.2f * part.CrewCapacity; } partmass = (FISmass + ArmorMass + FBmass); - Utils.RefreshAssociatedWindows(part); + GUIUtils.RefreshAssociatedWindows(part); using (List.Enumerator pSym = part.symmetryCounterparts.GetEnumerator()) while (pSym.MoveNext()) { @@ -178,16 +178,16 @@ public void TogglecockpitArmor() if (!armoredCockpit) { - tank.Events["TogglecockpitArmor"].guiName = Localizer.Format("#LOC_BDArmory_Armorcockpit_On");//"Enable self-sealing tank" + tank.Events["TogglecockpitArmor"].guiName = StringUtils.Localize("#LOC_BDArmory_Armorcockpit_On");//"Enable self-sealing tank" tank.ArmorMass = 0; } else { - tank.Events["TogglecockpitArmor"].guiName = Localizer.Format("#LOC_BDArmory_Armorcockpit_Off");//"Disable self-sealing tank" + tank.Events["TogglecockpitArmor"].guiName = StringUtils.Localize("#LOC_BDArmory_Armorcockpit_Off");//"Disable self-sealing tank" tank.ArmorMass = 0.2f * part.CrewCapacity; } tank.partmass = (tank.FISmass + tank.ArmorMass + tank.FBmass); - Utils.RefreshAssociatedWindows(pSym.Current); + GUIUtils.RefreshAssociatedWindows(pSym.Current); } if (HighLogic.LoadedSceneIsEditor && EditorLogic.fetch != null) GameEvents.onEditorShipModified.Fire(EditorLogic.fetch.ship); @@ -197,9 +197,9 @@ public void TogglecockpitArmor() [KSPField(advancedTweakable = true, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_AddedMass")]//safety systems mass public float partmass = 0f; - private float FBmass = 0f; + public float FBmass { get; private set; } = 0f; + public float FISmass { get; private set; } = 0f; private float ArmorMass = 0f; - private float FISmass = 0f; [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_FireBottles"),//Fire Bottles UI_FloatRange(minValue = 0, maxValue = 5, stepIncrement = 1, scene = UI_Scene.All, affectSymCounterparts = UI_Scene.All)] @@ -211,19 +211,27 @@ public void TogglecockpitArmor() Coroutine firebottleRoutine; PartResource fuel; + PartResource monoprop; PartResource solid; public bool isOnFire = false; - bool procWing = false; - + bool procPart = false; public bool externallyCalled = false; ModuleEngines engine; ModuleCommand cockpit; private float enginerestartTime = -1; public void Start() { - if (part.name.Contains("B9.Aero.Wing.Procedural")) //could add other proc parts here for similar support + if (part.name.Contains("B9.Aero.Wing.Procedural") || part.name.Contains("procedural")) //could add other proc parts here for similar support + { + procPart = true; + } + else { - procWing = true; + if (part.Modules.Contains("ModuleB9PartSwitch")) + { + var B9FuelSwitch = ConfigNodeUtils.FindPartModuleConfigNodeValue(part.partInfo.partConfig, "ModuleB9PartSwitch", "baseVolume"); + if (B9FuelSwitch != null) procPart = true; + } } if (HighLogic.LoadedSceneIsEditor) { @@ -247,6 +255,7 @@ public void Start() else { fuel = part.Resources.Where(pr => pr.resourceName == "LiquidFuel").FirstOrDefault(); + monoprop = part.Resources.Where(pr => pr.resourceName == "MonoPropellant").FirstOrDefault(); solid = part.Resources.Where(pr => pr.resourceName == "SolidFuel").FirstOrDefault(); engine = part.FindModuleImplementing(); @@ -256,7 +265,7 @@ public void Start() Events["ToggleInertOption"].guiActiveEditor = false; if (solid != null && engine.throttleLocked && !engine.allowShutdown) //SRB? { - if (fuel == null || (fuel != null && solid.maxAmount > fuel.maxAmount)) + if (fuel == null && monoprop == null || ((fuel != null && solid.maxAmount > fuel.maxAmount) || (monoprop != null && solid.maxAmount > monoprop.maxAmount))) { part.RemoveModule(this); //don't add firebottles to SRBs, but allow for the S1.5.5 MH soyuz tank with integrated seperatrons } @@ -268,7 +277,11 @@ public void Start() } } } - else if (fuel == null && solid == null) + else if (monoprop != null) + { + Events["ToggleInertOption"].guiActiveEditor = false; //inerting isn't going to do anything against a substance that contains its own oxidizer + } + else if (fuel == null && monoprop == null && solid == null) { Events["ToggleTankOption"].guiActiveEditor = false; Events["ToggleInertOption"].guiActiveEditor = false; @@ -280,34 +293,34 @@ public void Start() } if (!SSTank) { - Events["ToggleTankOption"].guiName = Localizer.Format("#LOC_BDArmory_SSTank_On");//"Enable self-sealing tank" + Events["ToggleTankOption"].guiName = StringUtils.Localize("#LOC_BDArmory_SSTank_On");//"Enable self-sealing tank" } else { - Events["ToggleTankOption"].guiName = Localizer.Format("#LOC_BDArmory_SSTank_Off");//"Disable self-sealing tank" + Events["ToggleTankOption"].guiName = StringUtils.Localize("#LOC_BDArmory_SSTank_Off");//"Disable self-sealing tank" } if (!InertTank) { - Events["ToggleInertOption"].guiName = Localizer.Format("#LOC_BDArmory_FIS_On");//"Enable self-sealing tank" + Events["ToggleInertOption"].guiName = StringUtils.Localize("#LOC_BDArmory_FIS_On");//"Enable self-sealing tank" FISmass = 0; } else { - Events["ToggleInertOption"].guiName = Localizer.Format("#LOC_BDArmory_FIS_Off");//"Disable self-sealing tank" + Events["ToggleInertOption"].guiName = StringUtils.Localize("#LOC_BDArmory_FIS_Off");//"Disable self-sealing tank" FISmass = 0.15f; Fields["FireBottles"].guiActiveEditor = false; Fields["FBRemaining"].guiActive = false; } - Utils.RefreshAssociatedWindows(part); + GUIUtils.RefreshAssociatedWindows(part); partmass = (FISmass + ArmorMass + FBmass); if (HighLogic.LoadedSceneIsEditor && EditorLogic.fetch != null) GameEvents.onEditorShipModified.Fire(EditorLogic.fetch.ship); if (HighLogic.LoadedSceneIsFlight) { - if (cockpit == null && engine == null && fuel == null) part.RemoveModule(this); //PWing with no tank + if (cockpit == null && engine == null && (fuel == null && monoprop == null)) part.RemoveModule(this); //PWing with no tank } FBSetup(null, null); - //Debug.Log("[SelfSealingTank] SST: " + SSTank + "; Inerting: " + InertTank + "; armored cockpit: " + armoredCockpit); + //Debug.Log("[BDArmory.SelfSealingTank]: SST: " + SSTank + "; Inerting: " + InertTank + "; armored cockpit: " + armoredCockpit); } /* public override void OnLoad(ConfigNode node) @@ -414,9 +427,9 @@ void FBSetup(BaseField field, object obj) tank.FBRemaining = FBRemaining; tank.partmass = partmass + FISmass + ArmorMass; tank.externallyCalled = false; - Utils.RefreshAssociatedWindows(pSym.Current); + GUIUtils.RefreshAssociatedWindows(pSym.Current); } - Utils.RefreshAssociatedWindows(part); + GUIUtils.RefreshAssociatedWindows(part); } public override string GetInfo() @@ -454,7 +467,7 @@ public void Extinguishtank() { firebottleRoutine = StartCoroutine(ExtinguishRoutine(4, true)); } - //Debug.Log("[SelfSealingTank] Fire detected; beginning ExtinguishRoutine. Firebottles remaining: " + FireBottles); + //Debug.Log("[BDArmory.SelfSealingTank]: Fire detected; beginning ExtinguishRoutine. Firebottles remaining: " + FireBottles); } } else @@ -468,7 +481,7 @@ public void Extinguishtank() if (firebottleRoutine == null) { firebottleRoutine = StartCoroutine(ExtinguishRoutine(10, false)); - //Debug.Log("[SelfSealingTank] Fire detected; beginning ExtinguishRoutine. Toggling Engine"); + //Debug.Log("[BDArmory.SelfSealingTank]: Fire detected; beginning ExtinguishRoutine. Toggling Engine"); } } //though if it is diving, then there isn't a second call to cycle engines. Add an Ienumerator to check once every couple sec? @@ -477,9 +490,9 @@ public void Extinguishtank() } IEnumerator ExtinguishRoutine(float time, bool useBottle) { - //Debug.Log("[SelfSealingTank] ExtinguishRoutine started. Time left: " + time); - yield return new WaitForSeconds(time); - //Debug.Log("[SelfSealingTank] Timer finished. Extinguishing"); + //Debug.Log("[BDArmory.SelfSealingTank]: ExtinguishRoutine started. Time left: " + time); + yield return new WaitForSecondsFixed(time); + //Debug.Log("[BDArmory.SelfSealingTank]: Timer finished. Extinguishing"); foreach (var existingFire in part.GetComponentsInChildren()) { if (!existingFire.surfaceFire) existingFire.burnTime = 0.05f; //kill all fires @@ -488,8 +501,8 @@ IEnumerator ExtinguishRoutine(float time, bool useBottle) { FireBottles--; FBRemaining = FireBottles; - Utils.RefreshAssociatedWindows(part); - //Debug.Log("[SelfSealingTank] Consuming firebottle. FB remaining: " + FireBottles); + GUIUtils.RefreshAssociatedWindows(part); + //Debug.Log("[BDArmory.SelfSealingTank]: Consuming firebottle. FB remaining: " + FireBottles); isOnFire = false; } ResetCoroutine(); @@ -507,16 +520,17 @@ void Update() { if (HighLogic.LoadedSceneIsEditor) { - if (procWing) + if (procPart) { - updateTimer -= Time.fixedDeltaTime; + updateTimer -= Time.deltaTime; if (updateTimer < 0) { fuel = part.Resources.Where(pr => pr.resourceName == "LiquidFuel").FirstOrDefault(); - if (fuel != null) + monoprop = part.Resources.Where(pr => pr.resourceName == "MonoPropellant").FirstOrDefault(); + if (fuel != null || monoprop != null) { Events["ToggleTankOption"].guiActiveEditor = true; - Events["ToggleInertOption"].guiActiveEditor = true; + if (fuel != null) Events["ToggleInertOption"].guiActiveEditor = true; //I don't think inerting would work on something containing its own oxidizer... if (!InertTank) { Fields["FireBottles"].guiActiveEditor = true; @@ -538,13 +552,19 @@ void Update() Fields["partmass"].guiActiveEditor = false; InertTank = false; FireBottles = 0; + FBmass = 0; + FBRemaining = 0; } - updateTimer = 0.5f; //doing it this way since PAw buttons don't seem to trigger onShipModified + updateTimer = 0.5f; //doing it this way since PAW buttons don't seem to trigger onShipModified } } } + } + void FixedUpdate() + { if (!HighLogic.LoadedSceneIsFlight || !FlightGlobals.ready || BDArmorySetup.GameIsPaused) return; // Not in flight scene, not ready or paused. if (vessel == null || vessel.packed || part == null) return; // Vessel or part is dead or packed. + if (!BDArmorySettings.BATTLEDAMAGE || BDArmorySettings.PEACE_MODE) return; if (!BDArmorySettings.BD_FIRES_ENABLED || !BDArmorySettings.BD_FIRE_HEATDMG) return; // Disabled. if (BDArmorySettings.BD_FIRES_ENABLED && BDArmorySettings.BD_FIRE_HEATDMG) @@ -552,7 +572,7 @@ void Update() if (InertTank) return; if (!isOnFire) { - if ((fuel != null && fuel.amount > 0) && part.temperature > 493) //autoignition temp of kerosene is 220 c + if (((fuel != null && fuel.amount > 0) || (monoprop != null && monoprop.amount > 0)) && part.temperature > 493) //autoignition temp of kerosene is 220 c. hydrazine is 24-270, so this works for monoprop as well { string fireStarter; var vesselFire = part.vessel.GetComponentInChildren(); @@ -565,7 +585,7 @@ void Update() fireStarter = part.vessel.GetName(); } BulletHitFX.AttachFire(transform.position, part, 50, fireStarter); - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[SelfSealingTank] Fuel auto-ignition! " + part.name + " is on fire! Fuel quantity: " + fuel.amount + "; temperature: " + part.temperature); + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log("[BDarmory.SelfSealingTank]: Fuel auto-ignition! " + part.name + " is on fire! Fuel quantity: " + fuel.amount + "; temperature: " + part.temperature); Extinguishtank(); isOnFire = true; } diff --git a/BDArmory/Modules/ModuleSpaceFriction.cs b/BDArmory/Modules/ModuleSpaceFriction.cs deleted file mode 100644 index 4b252e739..000000000 --- a/BDArmory/Modules/ModuleSpaceFriction.cs +++ /dev/null @@ -1,185 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using BDArmory.Core; -using UnityEngine; - -namespace BDArmory.Modules -{ - public class ModuleSpaceFriction : PartModule - { - /// - /// Adds friction/drag to craft in null-atmo porportional to AI MaxSpeed setting to ensure craft does not exceed said speed - /// Adds counter-gravity to prevent null-atmo ships from falling to the ground from gravity in the absence of wings and lift - /// Provides additional friction/drag during corners to help spacecraft drift through turns instead of being stuck with straight-up joust charges - /// TL;DR, provides the means for SciFi style space dogfights - /// - - private double frictionCoeff = 1.0f; //how much force is applied to decellerate craft - - //[KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "Space Friction"), UI_Toggle(disabledText = "Disabled", enabledText = "Enabled", scene = UI_Scene.All, affectSymCounterparts = UI_Scene.All)] - //public bool FrictionEnabled = false; //global value - - //[KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "CounterGrav"), UI_Toggle(disabledText = "Disabled", enabledText = "Enabled", scene = UI_Scene.All, affectSymCounterparts = UI_Scene.All)] - //public bool AntiGravEnabled = false; //global value - - [KSPField(isPersistant = true)] - public bool AntiGravOverride = false; //per craft override to be set in the .craft file, for things like zeppelin battles where attacking planes shouldn't be under countergrav - - public float maxVelocity = 300; //MaxSpeed setting in PilotAI - - public float frictMult; //engine thrust of craft - - float vesselAlt = 25; - //public float driftMult = 2; //additional drag multipler for cornering/decellerating so things don't take the same amount of time to decelerate as they do to accelerate - - public static bool GameIsPaused - { - get { return PauseMenu.isOpen || Time.timeScale == 0; } - } - BDModulePilotAI AI; - public BDModulePilotAI pilot - { - get - { - if (AI) return AI; - AI = VesselModuleRegistry.GetBDModulePilotAI(vessel, true); // FIXME should this be IBDAIControl? - return AI; - } - } - BDModuleSurfaceAI SAI; - public BDModuleSurfaceAI driver - { - get - { - if (SAI) return SAI; - SAI = VesselModuleRegistry.GetBDModuleSurfaceAI(vessel, true); - return SAI; - } - } - ModuleEngines Engine; - public ModuleEngines foundEngine - { - get - { - if (Engine) return Engine; - Engine = VesselModuleRegistry.GetModuleEngines(vessel).FirstOrDefault(); - return Engine; - } - } - MissileFire MF; - public MissileFire weaponManager - { - get - { - if (MF) return MF; - MF = VesselModuleRegistry.GetMissileFire(vessel, true); - return MF; - } - } - void Start() - { - if (HighLogic.LoadedSceneIsFlight) - { - using (var engine = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) - while (engine.MoveNext()) - { - if (engine.Current == null) continue; - frictMult += (engine.Current.maxThrust * (engine.Current.thrustPercentage / 100)); - //have this called onvesselModified? - } - } - } - - public void FixedUpdate() - { - if (!BDArmorySettings.SPACE_HACKS || !HighLogic.LoadedSceneIsFlight || !FlightGlobals.ready || this.vessel.packed || GameIsPaused) return; - - if (this.part.vessel.situation == Vessel.Situations.FLYING || this.part.vessel.situation == Vessel.Situations.SUB_ORBITAL) - { - if (BDArmorySettings.SF_FRICTION) - { - if (this.part.vessel.speed > 10) - { - if (AI != null) - { - maxVelocity = AI.maxSpeed; - } - else if (SAI != null) - { - maxVelocity = SAI.MaxSpeed; - } - frictionCoeff = Mathf.Pow(((float)part.vessel.speed / maxVelocity), 3) * frictMult; //at maxSpeed, have friction be 100% of vessel's engines thrust - - frictionCoeff *= (1 + (Vector3.Angle(this.part.vessel.srf_vel_direction, this.part.vessel.GetTransform().up) / 180) * BDArmorySettings.SF_DRAGMULT); //greater AoA off prograde, greater drag - - part.vessel.rootPart.rb.AddForceAtPosition((-part.vessel.srf_vel_direction * frictionCoeff), part.vessel.CoM, ForceMode.Acceleration); - } - } - if (BDArmorySettings.SF_GRAVITY || AntiGravOverride) //have this disabled if no engines left? - { - if (weaponManager != null && foundEngine != null) //have engineless craft fall - { - for (int i = 0; i < part.vessel.Parts.Count; i++) - { - if (part.vessel.parts[i].PhysicsSignificance != 1) //attempting to apply rigidbody force to non-significant parts will NRE - { - part.vessel.Parts[i].Rigidbody.AddForce(-FlightGlobals.getGeeForceAtPosition(part.vessel.Parts[i].transform.position), ForceMode.Acceleration); - } - } - } - } - } - if (this.part.vessel.situation != Vessel.Situations.ORBITING || this.part.vessel.situation != Vessel.Situations.DOCKED || this.part.vessel.situation != Vessel.Situations.ESCAPING || this.part.vessel.situation != Vessel.Situations.PRELAUNCH) - { - if (BDArmorySettings.SF_REPULSOR) - { - if ((pilot != null || driver != null) && foundEngine != null) - { - vesselAlt = 10; - if (AI != null) - { - vesselAlt = AI.minAltitude; - } - else if (SAI != null) - { - vesselAlt = SAI.MaxSlopeAngle * 2; - } - float accelMult = 1f; - if (vessel.verticalSpeed > 1) //vessel ascending - { - accelMult = Mathf.Clamp(Mathf.Abs((float)vessel.verticalSpeed), 1f, 100); - } - if (vessel.radarAltitude < Mathf.Max((vesselAlt / 10), 5)) - { - accelMult = Mathf.Clamp((float)vessel.radarAltitude / vesselAlt, 0.3f, 1); - } - if (vessel.radarAltitude < vesselAlt) - { - for (int i = 0; i < part.vessel.Parts.Count; i++) - { - if (part.vessel.parts[i].PhysicsSignificance != 1) //attempting to apply rigidbody force to non-significant parts will NRE - { - part.vessel.Parts[i].Rigidbody.AddForce((-FlightGlobals.getGeeForceAtPosition(part.vessel.Parts[i].transform.position) * ((vesselAlt / part.vessel.radarAltitude)) / accelMult), ForceMode.Acceleration); - } - } - } - } - } - } - } - - public static void AddSpaceFrictionToAllValidVessels() - { - foreach (var vessel in FlightGlobals.Vessels) - { - if (VesselModuleRegistry.GetMissileFire(vessel, true) != null && vessel.rootPart.FindModuleImplementing() == null) - { - vessel.rootPart.AddModule("ModuleSpaceFriction"); - } - } - } - } -} diff --git a/BDArmory/Modules/ModuleSpaceRadar.cs b/BDArmory/Modules/ModuleSpaceRadar.cs deleted file mode 100644 index 734eddeb9..000000000 --- a/BDArmory/Modules/ModuleSpaceRadar.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Collections.Generic; - -namespace BDArmory.Modules -{ - public class ModuleSpaceRadar : ModuleRadar - { - public void Update() // runs every frame - { - if (HighLogic.LoadedSceneIsFlight) // if in the flight scene - { - UpdateRadar(); // run the UpdateRadar code - } - } - - // This code determines if the radar is below the cutoff altitude and if so then - // it disables the radar ... private so that it cannot be accessed by any other code - private void UpdateRadar() - { - if (vessel.atmDensity >= 0.007) // below an atm density of 0.007 the radar will not work - { - List radarParts = new List(200); // creates a list of parts with this module - - foreach (Part p in vessel.Parts) // checks each part in the vessel - { - radarParts.AddRange(p.FindModulesImplementing()); // adds the part to the list if this module is present in the part - } - foreach (ModuleSpaceRadar radarPart in radarParts) // for each of the parts in the list do the following - { - if (radarPart != null && radarPart.radarEnabled) - { - DisableRadar(); // disable the radar - } - } - } - } - } -} diff --git a/BDArmory/Modules/_description b/BDArmory/Modules/_description index b1e4bb811..dc49f8717 100644 --- a/BDArmory/Modules/_description +++ b/BDArmory/Modules/_description @@ -1,2 +1,2 @@ -Modules that get added to vessels or parts. -- FIXME This should probably be broken down into sections (and merged with other sections) instead of having them clumped together, e.g., Control, Weapons (Guns, Missiles, Ammo, Other), Armour, GameModes, Radar, Targeting, Utilities (vessel/part utils), Utils (code utils). \ No newline at end of file +Modules that get added to vessels or parts that don't fit in anywhere else. +FIXME Find better location for the remaining modules. \ No newline at end of file diff --git a/BDArmory/Parts/_description b/BDArmory/Parts/_description deleted file mode 100644 index 6226036fa..000000000 --- a/BDArmory/Parts/_description +++ /dev/null @@ -1 +0,0 @@ -FIXME What are these? They ought to be moved into relevant folders (Targeting, FX, etc.). \ No newline at end of file diff --git a/BDArmory/Properties/AssemblyInfo.cs b/BDArmory/Properties/AssemblyInfo.cs index 3cadb191c..31bf92f9d 100644 --- a/BDArmory/Properties/AssemblyInfo.cs +++ b/BDArmory/Properties/AssemblyInfo.cs @@ -17,8 +17,8 @@ // The form "{Major}.{Minor}.*" will automatically update the build and revision, // and "{Major}.{Minor}.{Build}.*" will update just the revision. -[assembly: AssemblyVersion("1.4.18.4")] -[assembly: AssemblyFileVersion("1.4.18.4")] +[assembly: AssemblyVersion("1.6.3.0")] +[assembly: AssemblyFileVersion("1.6.3.0")] // The following attributes are used to specify the signing key for the assembly, // if desired. See the Mono documentation for more information about signing. diff --git a/BDArmory/Radar/ModuleIRST.cs b/BDArmory/Radar/ModuleIRST.cs new file mode 100644 index 000000000..2dae07d70 --- /dev/null +++ b/BDArmory/Radar/ModuleIRST.cs @@ -0,0 +1,492 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text; +using UnityEngine; +using KSP.Localization; + +using BDArmory.Control; +using BDArmory.Extensions; +using BDArmory.Settings; +using BDArmory.Targeting; +using BDArmory.UI; +using BDArmory.Utils; +using BDArmory.WeaponMounts; + +namespace BDArmory.Radar +{ + public class ModuleIRST : PartModule + { + #region KSPFields (Part Configuration) + + #region General Configuration + + [KSPField] + public string IRSTName; + + [KSPField] + public int turretID = 0; + + [KSPField] + public string rotationTransformName = string.Empty; + Transform rotationTransform; + + #endregion General Configuration + + #region Capabilities + + [KSPField] + public double resourceDrain = 0.825; //resource (EC/sec) usage of active irst + + [KSPField] + public bool omnidirectional = true; //false=boresight only + + [KSPField] + public float directionalFieldOfView = 90; //relevant for omnidirectional only + + [KSPField] + public float boresightFOV = 10; //relevant for boresight only + + [KSPField] + public float scanRotationSpeed = 120; //in degrees per second, relevant for omni and directional + + [KSPField] + public bool showDirectionWhileScan = false; //irst can show direction indicator of contacts (false: can show contacts as blocks only) + + [KSPField] + public bool canScan = true; //irst has detection capabilities + + [KSPField] + public bool irstRanging = false; //irst can get ranging info for target distance + + [KSPField] + public FloatCurve DetectionCurve = new FloatCurve(); //FloatCurve setting default ranging capabilities of the IRST + + [KSPField] + public FloatCurve TempSensitivityCurve = new FloatCurve(); //FloatCurve setting default IR spectrum capabilities of the IRST + + [KSPField] + public FloatCurve atmAttenuationCurve = new FloatCurve(); //FloatCurve range increase/decrease based on atm density/temp, thinner/cooler air yields longer range returns + + + [KSPField] + public float GroundClutterFactor = 0.16f; //Factor defining how effective the irst is at detecting heatsigs against ambient ground temperature (0=ineffective, 1=fully effective) + //default to 0.16, IRSTs have about a 6th of the detection range for ground targets vs air targets. + + #endregion Capabilities + + #region Persisted State in flight + + [KSPField(isPersistant = true)] + public string linkedVesselID; + + [KSPField(isPersistant = true)] + public bool irstEnabled; + + [KSPField(isPersistant = true)] + public int rangeIndex = 99; + + [KSPField(isPersistant = true)] + public float currentAngle = 0; + + #endregion Persisted State in flight + + #endregion KSPFields (Part Configuration) + + #region KSP Events & Actions + + [KSPAction("Toggle IRST")] + public void AGEnable(KSPActionParam param) + { + if (irstEnabled) + { + DisableIRST(); + } + else + { + EnableIRST(); + } + } + + [KSPEvent(active = true, guiActive = true, guiActiveEditor = false, guiName = "#LOC_BDArmory_ToggleIRST")]//Toggle IRST - FIXME - Localize + public void Toggle() + { + if (irstEnabled) + { + DisableIRST(); + } + else + { + EnableIRST(); + } + } + + #endregion KSP Events & Actions + + #region Part members + + public float irstMinDistanceDetect + { + get { return DetectionCurve.minTime; } + } + + //[KSPField(isPersistant = false, guiActive = true, guiActiveEditor = true, guiName = "Detection Range")] + public float irstMaxDistanceDetect + { + get { return DetectionCurve.maxTime; } + } + + //GUI + private bool drawGUI; + public float signalPersistTime; + + //scanning + public Transform referenceTransform; + private float radialScanDirection = 1; + + public bool boresightScan; + + //locking + public bool slaveTurrets; + public ModuleTurret lockingTurret; + public bool lockingPitch = true; + public bool lockingYaw = true; + + //vessel + private MissileFire wpmr; + + public MissileFire weaponManager + { + get + { + if (wpmr != null && wpmr.vessel == vessel) return wpmr; + wpmr = VesselModuleRegistry.GetMissileFire(vessel, true); + return wpmr; + } + set { wpmr = value; } + } + + public VesselRadarData vesselRadarData; + private string myVesselID; + + // part state + private bool startupComplete; + public float leftLimit; + public float rightLimit; + + #endregion Part members + + void UpdateToggleGuiName() + { + Events["Toggle"].guiName = irstEnabled ? StringUtils.Localize("#autoLOC_bda_1000036") : StringUtils.Localize("#autoLOC_bda_1000037"); // fixme - fix localizations + } + + public void EnsureVesselRadarData() + { + if (vessel == null) return; + //myVesselID = vessel.id.ToString(); + + if (vesselRadarData != null && vesselRadarData.vessel == vessel) return; + vesselRadarData = vessel.gameObject.GetComponent(); + + if (vesselRadarData == null) + { + vesselRadarData = vessel.gameObject.AddComponent(); + vesselRadarData.weaponManager = weaponManager; + } + } + + public void EnableIRST() + { + EnsureVesselRadarData(); + irstEnabled = true; + + var mf = VesselModuleRegistry.GetMissileFire(vessel, true); + UpdateToggleGuiName(); + vesselRadarData.AddIRST(this); + } + + public void DisableIRST() + { + irstEnabled = false; + UpdateToggleGuiName(); + + if (vesselRadarData) + { + vesselRadarData.RemoveIRST(this); + } + using (var loadedvessels = BDATargetManager.LoadedVessels.GetEnumerator()) + while (loadedvessels.MoveNext()) + { + BDATargetManager.ClearRadarReport(loadedvessels.Current, weaponManager); //reset radar contact status + } + } + + void OnDestroy() + { + if (HighLogic.LoadedSceneIsFlight) + { + if (vesselRadarData) + { + vesselRadarData.RemoveIRST(this); + vesselRadarData.RemoveDataFromIRST(this); + } + } + } + + public override void OnStart(StartState state) + { + base.OnStart(state); + + if (HighLogic.LoadedSceneIsFlight) + { + myVesselID = vessel.id.ToString(); + + if (string.IsNullOrEmpty(IRSTName)) + { + IRSTName = part.partInfo.title; + } + + signalPersistTime = omnidirectional + ? 360 / (scanRotationSpeed + 5) + : directionalFieldOfView / (scanRotationSpeed + 5); + + if (rotationTransformName != string.Empty) + { + rotationTransform = part.FindModelTransform(rotationTransformName); + } + + referenceTransform = (new GameObject()).transform; + referenceTransform.parent = transform; + referenceTransform.localPosition = Vector3.zero; + + // fill TempSensitivityCurve with default values if not set by part config: + if (TempSensitivityCurve.minTime == float.MaxValue) + TempSensitivityCurve.Add(0f, 1f); + + List.Enumerator turr = part.FindModulesImplementing().GetEnumerator(); + while (turr.MoveNext()) + { + if (turr.Current == null) continue; + if (turr.Current.turretID != turretID) continue; + lockingTurret = turr.Current; + break; + } + turr.Dispose(); + + //GameEvents.onVesselGoOnRails.Add(OnGoOnRails); //not needed + EnsureVesselRadarData(); + StartCoroutine(StartUpRoutine()); + } + else if (HighLogic.LoadedSceneIsEditor) + { + //Editor only: + List.Enumerator tur = part.FindModulesImplementing().GetEnumerator(); + while (tur.MoveNext()) + { + if (tur.Current == null) continue; + if (tur.Current.turretID != turretID) continue; + lockingTurret = tur.Current; + break; + } + tur.Dispose(); + if (lockingTurret) + { + lockingTurret.Fields["minPitch"].guiActiveEditor = false; + lockingTurret.Fields["maxPitch"].guiActiveEditor = false; + lockingTurret.Fields["yawRange"].guiActiveEditor = false; + } + } + } + + IEnumerator StartUpRoutine() + { + if (BDArmorySettings.DEBUG_RADAR) + Debug.Log("[BDArmory.ModuleIRST]: StartupRoutine: " + IRSTName + " enabled: " + irstEnabled); + yield return new WaitWhile(() => !FlightGlobals.ready || (vessel is not null && (vessel.packed || !vessel.loaded))); + yield return new WaitForFixedUpdate(); + UpdateToggleGuiName(); + startupComplete = true; + } + + void Update() + { + drawGUI = (HighLogic.LoadedSceneIsFlight && FlightGlobals.ready && !vessel.packed && irstEnabled && + vessel.isActiveVessel && BDArmorySetup.GAME_UI_ENABLED && !MapView.MapIsEnabled); + } + + void FixedUpdate() + { + if (HighLogic.LoadedSceneIsFlight && FlightGlobals.ready && startupComplete) + { + if (!vessel.IsControllable && irstEnabled) + { + DisableIRST(); + } + + if (irstEnabled) + { + DrainElectricity(); //physics behaviour, thus moved here from update + + if (boresightScan) + { + BoresightScan(); + } + else if (canScan) + { + Scan(); + } + } + + if (!vessel.packed && irstEnabled) + { + if (omnidirectional) + { + referenceTransform.position = part.transform.position; + referenceTransform.rotation = + Quaternion.LookRotation(VectorUtils.GetNorthVector(transform.position, vessel.mainBody), + VectorUtils.GetUpDirection(transform.position)); + } + else + { + referenceTransform.position = part.transform.position; + referenceTransform.rotation = Quaternion.LookRotation(part.transform.up, + VectorUtils.GetUpDirection(referenceTransform.position)); + } + //UpdateInputs(); + } + } + } + + void LateUpdate() + { + if (HighLogic.LoadedSceneIsFlight && canScan) + { + UpdateModel(); + } + } + + void UpdateModel() + { + //model rotation + if (irstEnabled) + { + if (rotationTransform && canScan) + { + Vector3 direction; + + direction = Quaternion.AngleAxis(currentAngle, referenceTransform.up) * referenceTransform.forward; + + Vector3 localDirection = rotationTransform.parent.InverseTransformDirection(direction).ProjectOnPlanePreNormalized(Vector3.up); + if (localDirection != Vector3.zero) + { + rotationTransform.localRotation = Quaternion.Lerp(rotationTransform.localRotation, + Quaternion.LookRotation(localDirection, Vector3.up), 10 * TimeWarp.fixedDeltaTime); + } + } + } + else + { + if (rotationTransform) + { + rotationTransform.localRotation = Quaternion.Lerp(rotationTransform.localRotation, + Quaternion.identity, 5 * TimeWarp.fixedDeltaTime); + } + } + } + + void Scan() + { + float angleDelta = scanRotationSpeed * Time.fixedDeltaTime; + RadarUtils.IRSTUpdateScan(weaponManager, currentAngle, referenceTransform, boresightFOV, referenceTransform.position, this); + + if (omnidirectional) + { + currentAngle = Mathf.Repeat(currentAngle + angleDelta, 360); + } + else + { + currentAngle += radialScanDirection * angleDelta; + + if (Mathf.Abs(currentAngle) > directionalFieldOfView / 2) + { + currentAngle = Mathf.Sign(currentAngle) * directionalFieldOfView / 2; + radialScanDirection = -radialScanDirection; + } + } + } + + void BoresightScan() + { + currentAngle = Mathf.Lerp(currentAngle, 0, 0.08f); + RadarUtils.IRSTUpdateScan(weaponManager, currentAngle, referenceTransform, boresightFOV, referenceTransform.position, this); + } + + public void ReceiveContactData(TargetSignatureData contactData, float _magnitude) + { + if (vesselRadarData) + { + vesselRadarData.AddIRSTContact(this, contactData, _magnitude); + } + } + + + void OnGUI() + { + if (drawGUI) + { + if (boresightScan) + { + GUIUtils.DrawTextureOnWorldPos(transform.position + (3500 * transform.up), + BDArmorySetup.Instance.dottedLargeGreenCircle, new Vector2(156, 156), 0); + } + } + } + + // RMB info in editor + public override string GetInfo() + { + StringBuilder output = new StringBuilder(); + output.Append(Environment.NewLine); + output.AppendLine(StringUtils.Localize("#autoLOC_bda_1000008", omnidirectional ? StringUtils.Localize("#autoLOC_bda_1000019") : StringUtils.Localize("#autoLOC_bda_1000020"))); + + output.AppendLine(StringUtils.Localize("#autoLOC_bda_1000021", resourceDrain)); //Ec/sec + + output.AppendLine(StringUtils.Localize("#autoLOC_bda_1000022", directionalFieldOfView)); //Field of View + + output.Append(Environment.NewLine); + output.AppendLine(StringUtils.Localize("#autoLOC_bda_1000024")); //Capabilities + output.AppendLine(StringUtils.Localize("#autoLOC_bda_1000025", canScan)); //-Scanning + + output.Append(Environment.NewLine); + output.AppendLine(StringUtils.Localize("#autoLOC_bda_1000030")); //Performance + + if (canScan) + output.AppendLine(StringUtils.Localize("#autoLOC_bda_1000031", DetectionCurve.Evaluate(irstMaxDistanceDetect)-273, irstMaxDistanceDetect)); //Detection x.xx deg C @ n km + else + output.AppendLine(StringUtils.Localize("#autoLOC_bda_1000032")); + + output.AppendLine(StringUtils.Localize("#autoLOC_bda_1000034")); + output.AppendLine(StringUtils.Localize("#autoLOC_bda_1000035", GroundClutterFactor)); + + + return output.ToString(); + } + + void DrainElectricity() + { + if (resourceDrain <= 0) + { + return; + } + + double drainAmount = resourceDrain * TimeWarp.fixedDeltaTime; + double chargeAvailable = part.RequestResource("ElectricCharge", drainAmount, ResourceFlowMode.ALL_VESSEL); + if (chargeAvailable < drainAmount * 0.95f) + { + ScreenMessages.PostScreenMessage(StringUtils.Localize("#autoLOC_bda_1000016"), 5.0f, ScreenMessageStyle.UPPER_CENTER); // #autoLOC_bda_1000016 = Radar Requires EC + DisableIRST(); + } + } + } +} diff --git a/BDArmory/Modules/ModuleRadar.cs b/BDArmory/Radar/ModuleRadar.cs similarity index 81% rename from BDArmory/Modules/ModuleRadar.cs rename to BDArmory/Radar/ModuleRadar.cs index 75e5f43aa..547d71c9e 100644 --- a/BDArmory/Modules/ModuleRadar.cs +++ b/BDArmory/Radar/ModuleRadar.cs @@ -2,15 +2,18 @@ using System.Collections; using System.Collections.Generic; using System.Text; -using BDArmory.Core; -using BDArmory.Misc; -using BDArmory.Radar; -using BDArmory.Targeting; -using BDArmory.UI; using UnityEngine; using KSP.Localization; -namespace BDArmory.Modules +using BDArmory.Control; +using BDArmory.Extensions; +using BDArmory.Settings; +using BDArmory.Targeting; +using BDArmory.UI; +using BDArmory.Utils; +using BDArmory.WeaponMounts; + +namespace BDArmory.Radar { public class ModuleRadar : PartModule { @@ -79,7 +82,10 @@ public class ModuleRadar : PartModule public bool canTrackWhileScan = false; //when tracking/locking, can we still detect/scan? [KSPField] - public bool canRecieveRadarData = false; //can radar data be received from friendly sources? + public bool canReceiveRadarData = false; //can radar data be received from friendly sources? + + [KSPField] // DEPRECATED + public bool canRecieveRadarData = false; // Original mis-spelling of "receive" for compatibility. [KSPField] public FloatCurve radarDetectionCurve = new FloatCurve(); //FloatCurve defining at what range which RCS size can be detected @@ -277,7 +283,7 @@ public MissileFire weaponManager void UpdateToggleGuiName() { - Events["Toggle"].guiName = radarEnabled ? Localizer.Format("#autoLOC_bda_1000000") : Localizer.Format("#autoLOC_bda_1000001"); // #autoLOC_bda_1000000 = Disable Radar // #autoLOC_bda_1000001 = Enable Radar + Events["Toggle"].guiName = radarEnabled ? StringUtils.Localize("#autoLOC_bda_1000000") : StringUtils.Localize("#autoLOC_bda_1000001"); // #autoLOC_bda_1000000 = Disable Radar // #autoLOC_bda_1000001 = Enable Radar } public void EnsureVesselRadarData() @@ -304,6 +310,11 @@ public void EnableRadar() if (mf != null && vesselRadarData != null) vesselRadarData.weaponManager = mf; UpdateToggleGuiName(); vesselRadarData.AddRadar(this); + if (mf != null) + { + if (mf.guardMode) vesselRadarData.LinkAllRadars(); + mf._radarsEnabled = true; + } } public void DisableRadar() @@ -328,6 +339,30 @@ public void DisableRadar() vrd.Current.UnlinkDisabledRadar(this); } vrd.Dispose(); + using (var loadedvessels = BDATargetManager.LoadedVessels.GetEnumerator()) + while (loadedvessels.MoveNext()) + { + BDATargetManager.ClearRadarReport(loadedvessels.Current, weaponManager); //reset radar contact status + } + var mf = VesselModuleRegistry.GetMissileFire(vessel, true); + if (mf != null) + { + if (mf.radars.Count > 1) + { + using (List.Enumerator rd = mf.radars.GetEnumerator()) + while (rd.MoveNext()) + { + if (rd.Current == null) continue; + mf._radarsEnabled = false; + if (rd.Current != this && rd.Current.radarEnabled) + { + mf._radarsEnabled = true; + break; + } + } + } + else mf._radarsEnabled = false; + } } void OnDestroy() @@ -438,6 +473,12 @@ public override void OnStart(StartState state) { Debug.Log("[BDArmory.ModuleRadar]: WARNING: " + part.name + " has legacy definition, missing new radarDetectionCurve and radarLockTrackCurve definitions! Please update for the part to be usable!"); } + + if (canRecieveRadarData) + { + Debug.LogWarning($"[BDArmory.ModuleRadar]: Radar part {part.name} is using deprecated 'canRecieveRadarData' attribute. Please update the config to use 'canReceiveRadarData' instead."); + canReceiveRadarData = canRecieveRadarData; + } } /* @@ -451,13 +492,9 @@ void OnGoOnRails(Vessel v) IEnumerator StartUpRoutine() { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_RADAR) Debug.Log("[BDArmory.ModuleRadar]: StartupRoutine: " + radarName + " enabled: " + radarEnabled); - while (!FlightGlobals.ready || vessel.packed) - { - yield return null; - } - + yield return new WaitWhile(() => !FlightGlobals.ready || vessel.packed || !vessel.loaded); yield return new WaitForFixedUpdate(); // DISABLE RADAR @@ -468,8 +505,6 @@ IEnumerator StartUpRoutine() } */ - yield return null; - if (!vesselRadarData.hasLoadedExternalVRDs) { RecoverLinkedVessels(); @@ -482,24 +517,6 @@ IEnumerator StartUpRoutine() void Update() { - if (HighLogic.LoadedSceneIsFlight && FlightGlobals.ready && !vessel.packed && radarEnabled) - { - if (omnidirectional) - { - referenceTransform.position = part.transform.position; - referenceTransform.rotation = - Quaternion.LookRotation(VectorUtils.GetNorthVector(transform.position, vessel.mainBody), - VectorUtils.GetUpDirection(transform.position)); - } - else - { - referenceTransform.position = part.transform.position; - referenceTransform.rotation = Quaternion.LookRotation(part.transform.up, - VectorUtils.GetUpDirection(referenceTransform.position)); - } - //UpdateInputs(); - } - drawGUI = (HighLogic.LoadedSceneIsFlight && FlightGlobals.ready && !vessel.packed && radarEnabled && vessel.isActiveVessel && BDArmorySetup.GAME_UI_ENABLED && !MapView.MapIsEnabled); } @@ -538,6 +555,23 @@ void FixedUpdate() Scan(); } } + if (!vessel.packed && radarEnabled) + { + if (omnidirectional) + { + referenceTransform.position = part.transform.position; + referenceTransform.rotation = + Quaternion.LookRotation(VectorUtils.GetNorthVector(transform.position, vessel.mainBody), + VectorUtils.GetUpDirection(transform.position)); + } + else + { + referenceTransform.position = part.transform.position; + referenceTransform.rotation = Quaternion.LookRotation(part.transform.up, + VectorUtils.GetUpDirection(referenceTransform.position)); + } + //UpdateInputs(); + } } } @@ -583,8 +617,7 @@ void UpdateModel() direction = Quaternion.AngleAxis(currentAngle, referenceTransform.up) * referenceTransform.forward; } - Vector3 localDirection = - Vector3.ProjectOnPlane(rotationTransform.parent.InverseTransformDirection(direction), Vector3.up); + Vector3 localDirection = rotationTransform.parent.InverseTransformDirection(direction).ProjectOnPlanePreNormalized(Vector3.up); if (localDirection != Vector3.zero) { rotationTransform.localRotation = Quaternion.Lerp(rotationTransform.localRotation, @@ -635,9 +668,7 @@ void Scan() if (locked) { - float targetAngle = VectorUtils.SignedAngle(referenceTransform.forward, - Vector3.ProjectOnPlane(lockedTarget.position - referenceTransform.position, - referenceTransform.up), referenceTransform.right); + float targetAngle = VectorUtils.SignedAngle(referenceTransform.forward, (lockedTarget.position - referenceTransform.position).ProjectOnPlanePreNormalized(referenceTransform.up), referenceTransform.right); leftLimit = Mathf.Clamp(targetAngle - (multiLockFOV / 2), -directionalFieldOfView / 2, directionalFieldOfView / 2); rightLimit = Mathf.Clamp(targetAngle + (multiLockFOV / 2), -directionalFieldOfView / 2, @@ -672,7 +703,7 @@ public bool TryLockTarget(Vector3 position, Vessel targetVessel = null) return false; } - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_RADAR) { if (targetVessel == null) Debug.Log("[BDArmory.ModuleRadar]: Trying to radar lock target with (" + radarName + ")"); @@ -682,13 +713,12 @@ public bool TryLockTarget(Vector3 position, Vessel targetVessel = null) if (currentLocks == maxLocks) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_RADAR) Debug.Log("[BDArmory.ModuleRadar]: - Failed, this radar already has the maximum allowed targets locked."); return false; } - Vector3 targetPlanarDirection = Vector3.ProjectOnPlane(position - referenceTransform.position, - referenceTransform.up); + Vector3 targetPlanarDirection = (position - referenceTransform.position).ProjectOnPlanePreNormalized(referenceTransform.up); float angle = Vector3.Angle(targetPlanarDirection, referenceTransform.forward); if (referenceTransform.InverseTransformPoint(position).x < 0) { @@ -707,15 +737,13 @@ public bool TryLockTarget(Vector3 position, Vessel targetVessel = null) if (!locked && !omnidirectional) { - float targetAngle = VectorUtils.SignedAngle(referenceTransform.forward, - Vector3.ProjectOnPlane(attemptedLocks[i].position - referenceTransform.position, - referenceTransform.up), referenceTransform.right); + float targetAngle = VectorUtils.SignedAngle(referenceTransform.forward, (attemptedLocks[i].position - referenceTransform.position).ProjectOnPlanePreNormalized(referenceTransform.up), referenceTransform.right); currentAngle = targetAngle; } lockedTargets.Add(attemptedLocks[i]); currLocks = lockedTargets.Count; - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_RADAR) Debug.Log("[BDArmory.ModuleRadar]: - Acquired lock on target (" + (attemptedLocks[i].vessel != null ? attemptedLocks[i].vessel.name : null) + ")"); vesselRadarData.AddRadarContact(this, lockedTarget, true); @@ -724,7 +752,7 @@ public bool TryLockTarget(Vector3 position, Vessel targetVessel = null) } } - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_RADAR) Debug.Log("[BDArmory.ModuleRadar]: - Failed to lock on target."); return false; @@ -754,7 +782,7 @@ void UpdateLock(int index) { TargetSignatureData lockedTarget = lockedTargets[index]; - Vector3 targetPlanarDirection = Vector3.ProjectOnPlane(lockedTarget.predictedPosition - referenceTransform.position, referenceTransform.up); + Vector3 targetPlanarDirection = (lockedTarget.predictedPosition - referenceTransform.position).ProjectOnPlanePreNormalized(referenceTransform.up); float lookAngle = Vector3.Angle(targetPlanarDirection, referenceTransform.forward); if (referenceTransform.InverseTransformPoint(lockedTarget.predictedPosition).x < 0) { @@ -835,7 +863,7 @@ public void UnlockAllTargets() vesselRadarData.UnlockAllTargetsOfRadar(this); } - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_RADAR) Debug.Log("[BDArmory.ModuleRadar]: Radar Targets were cleared (" + radarName + ")."); } @@ -853,6 +881,11 @@ public void SetActiveLock(TargetSignatureData target) public void UnlockTargetAt(int index, bool tryRelock = false) { + if (index < 0 || index >= lockedTargets.Count) + { + if (BDArmorySettings.DEBUG_RADAR) Debug.Log($"[BDArmory.ModuleRadar]: invalid index {index} for lockedTargets of size {lockedTargets.Count}"); + return; + } Vessel rVess = lockedTargets[index].vessel; if (tryRelock) @@ -884,8 +917,9 @@ public void UnlockTargetAt(int index, bool tryRelock = false) IEnumerator RetryLockRoutine(Vessel v) { - yield return null; - vesselRadarData.TryLockTarget(v); + yield return new WaitForFixedUpdate(); + if (vesselRadarData != null && vesselRadarData.isActiveAndEnabled) + vesselRadarData.TryLockTarget(v); } public void UnlockTargetVessel(Vessel v) @@ -997,7 +1031,7 @@ void OnGUI() { if (boresightScan) { - BDGUIUtils.DrawTextureOnWorldPos(transform.position + (3500 * transform.up), + GUIUtils.DrawTextureOnWorldPos(transform.position + (3500 * transform.up), BDArmorySetup.Instance.dottedLargeGreenCircle, new Vector2(156, 156), 0); } } @@ -1027,20 +1061,17 @@ IEnumerator RecoverLinkedVesselRoutine(string vesselID) yield break; } - yield return new WaitForSeconds(0.5f); + yield return new WaitForSecondsFixed(0.5f); } } IEnumerator RelinkVRDWhenReadyRoutine(VesselRadarData vrd) { - while (!vrd.radarsReady || vrd.vessel.packed) - { - yield return null; - } - yield return null; + yield return new WaitWhile(() => !vrd.radarsReady || (vrd.vessel is not null && (vrd.vessel.packed || !vrd.vessel.loaded))); + yield return new WaitForFixedUpdate(); + if (vrd.vessel is null) yield break; vesselRadarData.LinkVRD(vrd); - Debug.Log("[BDArmory.ModuleRadar]: Radar data link recovered: Local - " + vessel.vesselName + ", External - " + - vrd.vessel.vesselName); + if (BDArmorySettings.DEBUG_RADAR) Debug.Log("[BDArmory.ModuleRadar]: Radar data link recovered: Local - " + vessel.vesselName + ", External - " + vrd.vessel.vesselName); } public string getRWRType(int i) @@ -1048,66 +1079,66 @@ public string getRWRType(int i) switch (i) { case 0: - return Localizer.Format("#autoLOC_bda_1000002"); // #autoLOC_bda_1000002 = SAM + return StringUtils.Localize("#autoLOC_bda_1000002"); // #autoLOC_bda_1000002 = SAM case 1: - return Localizer.Format("#autoLOC_bda_1000003"); // #autoLOC_bda_1000003 = FIGHTER + return StringUtils.Localize("#autoLOC_bda_1000003"); // #autoLOC_bda_1000003 = FIGHTER case 2: - return Localizer.Format("#autoLOC_bda_1000004"); // #autoLOC_bda_1000004 = AWACS + return StringUtils.Localize("#autoLOC_bda_1000004"); // #autoLOC_bda_1000004 = AWACS case 3: case 4: - return Localizer.Format("#autoLOC_bda_1000005"); // #autoLOC_bda_1000005 = MISSILE + return StringUtils.Localize("#autoLOC_bda_1000005"); // #autoLOC_bda_1000005 = MISSILE case 5: - return Localizer.Format("#autoLOC_bda_1000006"); // #autoLOC_bda_1000006 = DETECTION + return StringUtils.Localize("#autoLOC_bda_1000006"); // #autoLOC_bda_1000006 = DETECTION case 6: - return Localizer.Format("#autoLOC_bda_1000017"); // #autoLOC_bda_1000017 = SONAR + return StringUtils.Localize("#autoLOC_bda_1000017"); // #autoLOC_bda_1000017 = SONAR } - return Localizer.Format("#autoLOC_bda_1000007"); // #autoLOC_bda_1000007 = UNKNOWN + return StringUtils.Localize("#autoLOC_bda_1000007"); // #autoLOC_bda_1000007 = UNKNOWN //{SAM = 0, Fighter = 1, AWACS = 2, MissileLaunch = 3, MissileLock = 4, Detection = 5, Sonar = 6} } // RMB info in editor public override string GetInfo() { - bool isLinkOnly = (canRecieveRadarData && !canScan && !canLock); + bool isLinkOnly = (canReceiveRadarData && !canScan && !canLock); StringBuilder output = new StringBuilder(); output.Append(Environment.NewLine); - output.AppendLine(Localizer.Format("#autoLOC_bda_1000008", (isLinkOnly ? Localizer.Format("#autoLOC_bda_1000018") : omnidirectional ? Localizer.Format("#autoLOC_bda_1000019") : Localizer.Format("#autoLOC_bda_1000020")))); + output.AppendLine(StringUtils.Localize("#autoLOC_bda_1000008", (isLinkOnly ? StringUtils.Localize("#autoLOC_bda_1000018") : omnidirectional ? StringUtils.Localize("#autoLOC_bda_1000019") : StringUtils.Localize("#autoLOC_bda_1000020")))); - output.AppendLine(Localizer.Format("#autoLOC_bda_1000021", resourceDrain)); + output.AppendLine(StringUtils.Localize("#autoLOC_bda_1000021", resourceDrain)); if (!isLinkOnly) { - output.AppendLine(Localizer.Format("#autoLOC_bda_1000022", directionalFieldOfView)); - output.AppendLine(Localizer.Format("#autoLOC_bda_1000023", getRWRType(rwrThreatType))); + output.AppendLine(StringUtils.Localize("#autoLOC_bda_1000022", directionalFieldOfView)); + output.AppendLine(StringUtils.Localize("#autoLOC_bda_1000023", getRWRType(rwrThreatType))); output.Append(Environment.NewLine); - output.AppendLine(Localizer.Format("#autoLOC_bda_1000024")); - output.AppendLine(Localizer.Format("#autoLOC_bda_1000025", canScan)); - output.AppendLine(Localizer.Format("#autoLOC_bda_1000026", canTrackWhileScan)); - output.AppendLine(Localizer.Format("#autoLOC_bda_1000027", canLock)); + output.AppendLine(StringUtils.Localize("#autoLOC_bda_1000024")); + output.AppendLine(StringUtils.Localize("#autoLOC_bda_1000025", canScan)); + output.AppendLine(StringUtils.Localize("#autoLOC_bda_1000026", canTrackWhileScan)); + output.AppendLine(StringUtils.Localize("#autoLOC_bda_1000027", canLock)); if (canLock) { - output.AppendLine(Localizer.Format("#autoLOC_bda_1000028", maxLocks)); + output.AppendLine(StringUtils.Localize("#autoLOC_bda_1000028", maxLocks)); } - output.AppendLine(Localizer.Format("#autoLOC_bda_1000029", canRecieveRadarData)); + output.AppendLine(StringUtils.Localize("#autoLOC_bda_1000029", canReceiveRadarData)); output.Append(Environment.NewLine); - output.AppendLine(Localizer.Format("#autoLOC_bda_1000030")); + output.AppendLine(StringUtils.Localize("#autoLOC_bda_1000030")); if (canScan) - output.AppendLine(Localizer.Format("#autoLOC_bda_1000031", radarDetectionCurve.Evaluate(radarMaxDistanceDetect), radarMaxDistanceDetect)); + output.AppendLine(StringUtils.Localize("#autoLOC_bda_1000031", radarDetectionCurve.Evaluate(radarMaxDistanceDetect), radarMaxDistanceDetect)); else - output.AppendLine(Localizer.Format("#autoLOC_bda_1000032")); + output.AppendLine(StringUtils.Localize("#autoLOC_bda_1000032")); if (canLock) - output.AppendLine(Localizer.Format("#autoLOC_bda_1000033", radarLockTrackCurve.Evaluate(radarMaxDistanceLockTrack), radarMaxDistanceLockTrack)); + output.AppendLine(StringUtils.Localize("#autoLOC_bda_1000033", radarLockTrackCurve.Evaluate(radarMaxDistanceLockTrack), radarMaxDistanceLockTrack)); else - output.AppendLine(Localizer.Format("#autoLOC_bda_1000034")); - output.AppendLine(Localizer.Format("#autoLOC_bda_1000035", radarGroundClutterFactor)); + output.AppendLine(StringUtils.Localize("#autoLOC_bda_1000034")); + output.AppendLine(StringUtils.Localize("#autoLOC_bda_1000035", radarGroundClutterFactor)); } return output.ToString(); @@ -1124,7 +1155,7 @@ void DrainElectricity() double chargeAvailable = part.RequestResource("ElectricCharge", drainAmount, ResourceFlowMode.ALL_VESSEL); if (chargeAvailable < drainAmount * 0.95f) { - ScreenMessages.PostScreenMessage(Localizer.Format("#autoLOC_bda_1000016"), 5.0f, ScreenMessageStyle.UPPER_CENTER); // #autoLOC_bda_1000016 = Radar Requires EC + ScreenMessages.PostScreenMessage(StringUtils.Localize("#autoLOC_bda_1000016"), 5.0f, ScreenMessageStyle.UPPER_CENTER); // #autoLOC_bda_1000016 = Radar Requires EC DisableRadar(); } } diff --git a/BDArmory/Radar/ModuleSpaceRadar.cs b/BDArmory/Radar/ModuleSpaceRadar.cs new file mode 100644 index 000000000..7cc467d59 --- /dev/null +++ b/BDArmory/Radar/ModuleSpaceRadar.cs @@ -0,0 +1,21 @@ +namespace BDArmory.Radar +{ + public class ModuleSpaceRadar : ModuleRadar + { + void FixedUpdate() + { + if (!HighLogic.LoadedSceneIsFlight) return; + UpdateRadar(); + } + + // This code determines if the radar is below the cutoff altitude and if so then it disables the radar... + void UpdateRadar() + { + if (!radarEnabled) return; + if (vessel.atmDensity >= 0.007) // above an atm density of 0.007 the radar will not work + { + DisableRadar(); // disable the radar + } + } + } +} diff --git a/BDArmory/Radar/RadarDisplayData.cs b/BDArmory/Radar/RadarDisplayData.cs index 9202ab943..15aa12ca3 100644 --- a/BDArmory/Radar/RadarDisplayData.cs +++ b/BDArmory/Radar/RadarDisplayData.cs @@ -13,4 +13,13 @@ public struct RadarDisplayData public TargetSignatureData targetData; public float signalPersistTime; } + public struct IRSTDisplayData + { + public Vessel vessel; + public Vector2 pingPosition; + public float magnitude; + public ModuleIRST detectedByIRST; + public TargetSignatureData targetData; + public float signalPersistTime; + } } diff --git a/BDArmory/Radar/RadarUtils.cs b/BDArmory/Radar/RadarUtils.cs index e005d3796..29e2a8d2e 100644 --- a/BDArmory/Radar/RadarUtils.cs +++ b/BDArmory/Radar/RadarUtils.cs @@ -1,15 +1,17 @@ +using System; using System.Collections.Generic; +using UnityEngine; + using BDArmory.Control; -using BDArmory.Core; -using BDArmory.Core.Extension; -using BDArmory.Core.Utils; using BDArmory.CounterMeasure; -using BDArmory.Misc; -using BDArmory.Modules; +using BDArmory.Extensions; +using BDArmory.Settings; using BDArmory.Shaders; using BDArmory.Targeting; using BDArmory.UI; -using UnityEngine; +using BDArmory.Utils; +using BDArmory.Weapons; +using BDArmory.Weapons.Missiles; namespace BDArmory.Radar { @@ -63,7 +65,7 @@ public static class RadarUtils internal static float rcsTotal; // dito - internal const float RCS_NORMALIZATION_FACTOR = 3.8f; //IMPORTANT FOR RCS CALCULATION! DO NOT CHANGE! (sphere with 1m^2 cross section should have 1m^2 RCS) + internal const float RCS_NORMALIZATION_FACTOR = 3.04f; //IMPORTANT FOR RCS CALCULATION! DO NOT CHANGE! (sphere with 1m^2 cross section should have 1m^2 RCS) internal const float RCS_MISSILES = 999f; //default rcs value for missiles if not configured in the part config internal const float RWR_PING_RANGE_FACTOR = 2.0f; internal const float RADAR_IGNORE_DISTANCE_SQR = 100f; @@ -120,6 +122,8 @@ public static class RadarUtils }; private static int numAspects = rcsAspects.GetLength(0); // Number of aspects public static float[,] worstRCSAspects = new float[3, 3]; // Worst three aspects + static double[] rcsValues; + static Color32[] pixels; /// /// Force radar signature update @@ -143,9 +147,10 @@ public static TargetInfo GetVesselRadarSignature(Vessel v) { //1. baseSig = GetVesselRadarCrossSection TargetInfo ti = GetVesselRadarCrossSection(v); - //2. modifiedSig = GetVesselModifiedSignature(baseSig) //ECM-jammers with rcs reduction effect; other rcs reductions (stealth) - float modifiedSig = GetVesselModifiedSignature(v, ti); + ti.radarRCSReducedSignature = ti.radarBaseSignature; //These are needed for Radar functions to work! + ti.radarModifiedSignature = ti.radarBaseSignature; + //ti.radarLockbreakFactor = 1; return ti; } @@ -160,7 +165,6 @@ private static TargetInfo GetVesselRadarCrossSection(Vessel v, bool force = fals if (ti == null) { - // add targetinfo to vessel ti = v.gameObject.AddComponent(); } @@ -197,42 +201,14 @@ private static TargetInfo GetVesselRadarCrossSection(Vessel v, bool force = fals ti.radarBaseSignatureNeedsUpdate = false; ti.alreadyScheduledRCSUpdate = false; ti.radarMassAtUpdate = v.GetTotalMass(); - } - return ti; - } - - /// - /// Internal method: get a vessels siganture modifiers (ecm, stealth, ...) - /// - private static float GetVesselModifiedSignature(Vessel v, TargetInfo ti) - { - ti.radarModifiedSignature = ti.radarBaseSignature; - ti.radarLockbreakFactor = 1; - - // - // read vessel ecminfo for active jammers and calculate effects: - VesselECMJInfo vesseljammer = v.gameObject.GetComponent(); - if (vesseljammer) - { - //1) read vessel ecminfo for jammers with RCS reduction effect and multiply factor - ti.radarModifiedSignature *= vesseljammer.rcsReductionFactor; - - //2) increase in detectability relative to jammerstrength and vessel rcs signature: - // rcs_factor = jammerStrength / modifiedSig / 100 + 1.0f - ti.radarModifiedSignature *= (((vesseljammer.jammerStrength / ti.radarModifiedSignature) / 100) + 1.0f); - - //3) garbling due to overly strong jamming signals relative to jammer's strength in relation to vessel rcs signature: - // jammingDistance = (jammerstrength / baseSig / 100 + 1.0) x js - ti.radarJammingDistance = ((vesseljammer.jammerStrength / ti.radarBaseSignature / 100) + 1.0f) * vesseljammer.jammerStrength; - - //4) lockbreaking strength relative to jammer's lockbreak strength in relation to vessel rcs signature: - // lockbreak_factor = baseSig/modifiedSig x (1 � lopckBreakStrength/baseSig/100) - // Use clamp to prevent RCS reduction resulting in increased lockbreak factor, which negates value of RCS reduction) - ti.radarLockbreakFactor = Mathf.Max(Mathf.Clamp01(ti.radarBaseSignature / ti.radarModifiedSignature) * (1 - (vesseljammer.lockBreakStrength / ti.radarBaseSignature / 100)), 0); // 0 is minimum lockbreak factor + // Update ECM impact on RCS if base RCS is modified + VesselECMJInfo jammer = v.gameObject.GetComponent(); + if (jammer != null) + jammer.UpdateJammerStrength(); } - return ti.radarModifiedSignature; + return ti; } /// @@ -265,8 +241,9 @@ public static float GetVesselECMJammingDistance(Vessel v) if (v == null) return jammingDistance; - jammingDistance = GetVesselRadarCrossSection(v).radarJammingDistance; - + var crossSection = GetVesselRadarCrossSection(v); + if (crossSection != null) + jammingDistance = crossSection.radarJammingDistance; return jammingDistance; } @@ -313,21 +290,21 @@ public static float RenderVesselRadarSnapshot(Vessel v, Transform t, bool inEdit Bounds vesselbounds = CalcVesselBounds(v, t); - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_RADAR) { if (HighLogic.LoadedSceneIsFlight) Debug.Log($"[BDArmory.RadarUtils]: Rendering radar snapshot of vessel {v.name}, type {v.vesselType}"); else Debug.Log("[BDArmory.RadarUtils]: Rendering radar snapshot of vessel"); - Debug.Log("[BDArmory.RadarUtils]: - bounds: " + vesselbounds.ToString()); - Debug.Log("[BDArmory.RadarUtils]: - rotation: " + t.rotation.ToString()); + Debug.Log($"[BDArmory.RadarUtils]: - bounds: {vesselbounds}"); + Debug.Log($"[BDArmory.RadarUtils]: - rotation: {t.rotation}"); //Debug.Log("[BDArmory.RadarUtils]: - size: " + vesselbounds.size + ", magnitude: " + vesselbounds.size.magnitude); } if (vesselbounds.size.sqrMagnitude == 0f) { // SAVE US THE RENDERING, result will be zero anyway... - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_RADAR) { Debug.Log("[BDArmory.RadarUtils]: - rcs is zero."); } @@ -340,11 +317,12 @@ public static float RenderVesselRadarSnapshot(Vessel v, Transform t, bool inEdit } float rcsVariable = 0f; - worstRCSAspects = new float[3, 3]; - double[] rcsValues = new double[numAspects]; + if (worstRCSAspects is null) worstRCSAspects = new float[3, 3]; + Array.Clear(worstRCSAspects, 0, 9); + if (rcsValues is null) rcsValues = new double[numAspects]; + Array.Clear(rcsValues, 0, numAspects); rcsTotal = 0; Vector3 aspect; - Color32[] pixels; // Loop through all aspects for (int i = 0; i < numAspects; i++) @@ -406,7 +384,7 @@ public static float RenderVesselRadarSnapshot(Vessel v, Transform t, bool inEdit } } - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_RADAR) { // Debug.Log($"[BDArmory.RadarUtils]: RCS Aspect Vector for (az/el) {rcsAspects[i, 0]}/{rcsAspects[i, 1]} is: " + aspect.ToString()); Debug.Log($"[BDArmory.RadarUtils]: - Vessel rcs for (az/el) is: {rcsAspects[i, 0]}/{rcsAspects[i, 1]} = rcsVariable: {rcsVariable}"); @@ -454,7 +432,7 @@ public static float RenderVesselRadarSnapshot(Vessel v, Transform t, bool inEdit } } - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_RADAR) { Debug.Log($"[BDArmory.RadarUtils]: - Vessel all-aspect rcs is: rcsTotal: {rcsTotal}"); } @@ -540,7 +518,7 @@ public static float RenderVesselRadarSnapshotLegacy(Vessel v, Transform t, bool v.SetPosition(v.transform.position + presentationPosition); Bounds vesselbounds = CalcVesselBounds(v, t); - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_RADAR) { if (HighLogic.LoadedSceneIsFlight) Debug.Log($"[BDArmory.RadarUtils]: Rendering radar snapshot of vessel {v.name}, type {v.vesselType}"); @@ -553,7 +531,7 @@ public static float RenderVesselRadarSnapshotLegacy(Vessel v, Transform t, bool if (vesselbounds.size.sqrMagnitude == 0f) { // SAVE US THE RENDERING, result will be zero anyway... - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_RADAR) { Debug.Log("[BDArmory.RadarUtils]: - rcs is zero."); } @@ -615,7 +593,7 @@ public static float RenderVesselRadarSnapshotLegacy(Vessel v, Transform t, bool rcsVentral45 /= RCS_NORMALIZATION_FACTOR; rcsTotal = (Mathf.Max(rcsFrontal, rcsFrontal45) + Mathf.Max(rcsLateral, rcsLateral45) + Mathf.Max(rcsVentral, rcsVentral45)) / 3f; - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_RADAR) { Debug.Log($"[BDArmory.RadarUtils]: - Vessel rcs is (frontal/lateral/ventral), (frontal45/lateral45/ventral45): {rcsFrontal}/{rcsLateral}/{rcsVentral}, {rcsFrontal45}/{rcsLateral45}/{rcsVentral45} = rcsTotal: {rcsTotal}"); } @@ -631,6 +609,7 @@ private static void RenderSinglePass(Transform t, bool inEditorZoom, Vector3 cam { // Render one snapshop pass: // setup camera FOV + radarCam.allowMSAA = false; // Don't allow MSAA with RCS render as this significantly affects results! radarCam.transform.position = vesselbounds.center + cameraDirection * radarDistance; radarCam.transform.LookAt(vesselbounds.center, -t.forward); float distanceToShip = Vector3.Distance(radarCam.transform.position, vesselbounds.center); @@ -795,7 +774,7 @@ public static void CleanupResourcesLegacy() /// /// Determine for a vesselposition relative to the radar position how much effect the ground clutter factor will have. /// - public static float GetRadarGroundClutterModifier(ModuleRadar radar, Transform referenceTransform, Vector3 position, Vector3 vesselposition, TargetInfo ti) + public static float GetRadarGroundClutterModifier(float clutterFactor, Transform referenceTransform, Vector3 position, Vector3 vesselposition, TargetInfo ti) { Vector3 upVector = referenceTransform.up; @@ -805,15 +784,67 @@ public static float GetRadarGroundClutterModifier(ModuleRadar radar, Transform r float lookDownAngle = angleFromUp - 90; // result range: -90 .. +90 Mathf.Clamp(lookDownAngle, 0, 90); // result range: 0 .. +90 - float groundClutterMutiplier = Mathf.Lerp(1, radar.radarGroundClutterFactor, (lookDownAngle / 90)); - + float groundClutterMutiplier = Mathf.Lerp(1, clutterFactor, (lookDownAngle / 90)); //additional ground clutter factor when target is landed/splashed: - if (ti.isLandedOrSurfaceSplashed || ti.isSplashed) - groundClutterMutiplier *= radar.radarGroundClutterFactor; + if (ti != null && (ti.isLandedOrSurfaceSplashed || ti.isSplashed)) + groundClutterMutiplier *= clutterFactor; return groundClutterMutiplier; } + /// + /// Determine how much of an effect enemies that are jamming have on the target + /// + public static float GetStandoffJammingModifier(Vessel v, Competition.BDTeam team, Vector3 position, Vessel targetV, float signature) + { + if (!VesselModuleRegistry.GetModule(targetV)) return 1f; // Don't evaluate SOJ effects for targets without weapons managers + if (signature == 0) return 1f; // Don't evaluate SOJ effects for targets with 0 signature + + float standOffJammingMod = 0f; + string debugSOJ = "Standoff Jammer Lockbreak Strengths: \n"; + + using (var loadedvessels = BDATargetManager.LoadedVessels.GetEnumerator()) + while (loadedvessels.MoveNext()) + { + + // ignore null, unloaded, self, teammates, the target and vessels without ECM + if (loadedvessels.Current == null || !loadedvessels.Current.loaded) continue; + if ((loadedvessels.Current == v) || (loadedvessels.Current == targetV)) continue; + if (loadedvessels.Current.vesselType == VesselType.Debris) continue; + + MissileFire wm = VesselModuleRegistry.GetModule(loadedvessels.Current); + + if (!wm) continue; + if (team.IsFriendly(wm.Team)) continue; + + VesselECMJInfo standOffJammer = loadedvessels.Current.gameObject.GetComponent(); + + if (standOffJammer && (standOffJammer.lockBreakStrength > 0)) + { + Vector3 relPositionJammer = loadedvessels.Current.CoM - position; + Vector3 relPositionTarget = targetV.CoM - position; + + // Modify total lockbreak strength of standoff jammer by angle off the vector to target + float angleModifier = Vector3.Dot(relPositionTarget.normalized, relPositionJammer.normalized); + float sojLBS = Mathf.Clamp01(angleModifier * angleModifier * angleModifier); + + // Modify lockbreak strength by relative sqr distance + sojLBS *= Mathf.Clamp(1 - Mathf.Log10(relPositionJammer.sqrMagnitude / relPositionTarget.sqrMagnitude), 0f, 3f); + + // Add up all stand up jammer lockbreaks + standOffJammingMod += sojLBS * standOffJammer.lockBreakStrength; + + if (BDArmorySettings.DEBUG_RADAR) debugSOJ += sojLBS * standOffJammer.lockBreakStrength + ", " + loadedvessels.Current.GetDisplayName() + "\n"; + } + } + + float modifiedSignature = Mathf.Max(signature - standOffJammingMod / 100f, 0f); + + if ((BDArmorySettings.DEBUG_RADAR) && (modifiedSignature != signature)) Debug.Log("[BDArmory.RadarUtils]: Standoff Jamming: " + targetV.GetDisplayName() + " signature relative to " + v.GetDisplayName() + " modified from " + signature + " to " + modifiedSignature + "\n" + debugSOJ); + + return modifiedSignature / signature; + } + /// /// Special scanning method that needs to be set manually on the radar: perform fixed boresight scan with locked fov. /// Called from ModuleRadar, which will then attempt to immediately lock onto the detected targets. @@ -850,36 +881,31 @@ public static bool RadarUpdateScanBoresight(Ray ray, float fov, ref TargetSignat // get vessel's radar signature TargetInfo ti = GetVesselRadarSignature(loadedvessels.Current); float signature = ti.radarModifiedSignature; - signature *= GetRadarGroundClutterModifier(radar, radar.referenceTransform, ray.origin, loadedvessels.Current.CoM, ti); + signature *= GetRadarGroundClutterModifier(radar.radarGroundClutterFactor, radar.referenceTransform, ray.origin, loadedvessels.Current.CoM, ti); + signature *= GetStandoffJammingModifier(radar.vessel, radar.weaponManager.Team, ray.origin, loadedvessels.Current, signature); // no ecm lockbreak factor here // no chaff factor here // evaluate range float distance = (loadedvessels.Current.CoM - ray.origin).magnitude / 1000f; //TODO: Performance! better if we could switch to sqrMagnitude... - if (distance > radar.radarMinDistanceDetect && distance < radar.radarMaxDistanceDetect) + if (RadarCanDetect(radar, signature, distance)) { - //evaluate if we can detect such a signature at that range - float minDetectSig = radar.radarDetectionCurve.Evaluate(distance); - - if (signature > minDetectSig) + // detected by radar + // fill attempted locks array for locking later: + while (dataIndex < dataArray.Length - 1) { - // detected by radar - // fill attempted locks array for locking later: - while (dataIndex < dataArray.Length - 1) + if (!dataArray[dataIndex].exists || (dataArray[dataIndex].exists && (Time.time - dataArray[dataIndex].timeAcquired) > dataPersistTime)) { - if (!dataArray[dataIndex].exists || (dataArray[dataIndex].exists && (Time.time - dataArray[dataIndex].timeAcquired) > dataPersistTime)) - { - break; - } - dataIndex++; + break; } + dataIndex++; + } - if (dataIndex < dataArray.Length) - { - dataArray[dataIndex] = new TargetSignatureData(loadedvessels.Current, signature); - dataIndex++; - hasLocked = true; - } + if (dataIndex < dataArray.Length) + { + dataArray[dataIndex] = new TargetSignatureData(loadedvessels.Current, signature); + dataIndex++; + hasLocked = true; } } @@ -911,7 +937,7 @@ public static bool RadarUpdateMissileLock(Ray ray, float fov, ref TargetSignatur while (loadedvessels.MoveNext()) { // ignore null, unloaded and ignored types - if (loadedvessels.Current == null || !loadedvessels.Current.loaded) continue; + if (loadedvessels.Current == null || loadedvessels.Current.packed || !loadedvessels.Current.loaded) continue; // IFF code check to prevent friendly lock-on (neutral vessel without a weaponmanager WILL be lockable!) MissileFire wm = VesselModuleRegistry.GetModule(loadedvessels.Current); @@ -935,10 +961,14 @@ public static bool RadarUpdateMissileLock(Ray ray, float fov, ref TargetSignatur // get vessel's radar signature TargetInfo ti = GetVesselRadarSignature(loadedvessels.Current); - float signature = ti.radarModifiedSignature; - // no ground clutter modifier for missiles - signature *= ti.radarLockbreakFactor; //multiply lockbreak factor from active ecm - //do not multiply chaff factor here + float signature = 10f; + if (ti != null) + { + signature = ti.radarModifiedSignature; + // no ground clutter modifier for missiles + signature *= ti.radarLockbreakFactor; //multiply lockbreak factor from active ecm + } //do not multiply chaff factor here + signature *= GetStandoffJammingModifier(missile.vessel, missile.Team, ray.origin, loadedvessels.Current, signature); // evaluate range float distance = (loadedvessels.Current.CoM - ray.origin).magnitude; @@ -1010,14 +1040,14 @@ public static bool RadarUpdateScanLock(MissileFire myWpnManager, float direction while (loadedvessels.MoveNext()) { // ignore null, unloaded and self - if (loadedvessels.Current == null || !loadedvessels.Current.loaded) continue; + if (loadedvessels.Current == null || loadedvessels.Current.packed || !loadedvessels.Current.loaded) continue; if (loadedvessels.Current == myWpnManager.vessel) continue; // ignore too close ones if ((loadedvessels.Current.transform.position - position).sqrMagnitude < RADAR_IGNORE_DISTANCE_SQR) continue; - Vector3 vesselDirection = Vector3.ProjectOnPlane(loadedvessels.Current.CoM - position, upVector); + Vector3 vesselDirection = (loadedvessels.Current.CoM - position).ProjectOnPlanePreNormalized(upVector); if (Vector3.Angle(vesselDirection, lookDirection) < fov / 2f) { // ignore when blocked by terrain @@ -1027,12 +1057,13 @@ public static bool RadarUpdateScanLock(MissileFire myWpnManager, float direction // get vessel's radar signature TargetInfo ti = GetVesselRadarSignature(loadedvessels.Current); float signature = ti.radarModifiedSignature; - //do not multiply chaff factor here - signature *= GetRadarGroundClutterModifier(radar, referenceTransform, position, loadedvessels.Current.CoM, ti); + //do not multiply chaff factor here + signature *= GetRadarGroundClutterModifier(radar.radarGroundClutterFactor, referenceTransform, position, loadedvessels.Current.CoM, ti); // evaluate range float distance = (loadedvessels.Current.CoM - position).magnitude / 1000f; //TODO: Performance! better if we could switch to sqrMagnitude... + BDATargetManager.ClearRadarReport(loadedvessels.Current, myWpnManager); if (modeTryLock) // LOCK/TRACK TARGET: { //evaluate if we can lock/track such a signature at that range @@ -1040,15 +1071,17 @@ public static bool RadarUpdateScanLock(MissileFire myWpnManager, float direction { //evaluate if we can lock/track such a signature at that range float minLockSig = radar.radarLockTrackCurve.Evaluate(distance); + signature *= ti.radarLockbreakFactor; //multiply lockbreak factor from active ecm //do not multiply chaff factor here + signature *= GetStandoffJammingModifier(radar.vessel, radar.weaponManager.Team, position, loadedvessels.Current, signature); - if (signature > minLockSig) + if (signature >= minLockSig && RadarCanDetect(radar, signature, distance)) // Must be able to detect and lock to lock targets { // detected by radar if (myWpnManager != null) { - BDATargetManager.ReportVessel(loadedvessels.Current, myWpnManager); + BDATargetManager.ReportVessel(loadedvessels.Current, myWpnManager, true); } // fill attempted locks array for locking later: @@ -1077,24 +1110,16 @@ public static bool RadarUpdateScanLock(MissileFire myWpnManager, float direction else // SCAN/DETECT TARGETS: { //evaluate if we can detect such a signature at that range - if (distance > radar.radarMinDistanceDetect && distance < radar.radarMaxDistanceDetect) + if (RadarCanDetect(radar, signature, distance)) { - //evaluate if we can detect or lock such a signature at that range - float minDetectSig = radar.radarDetectionCurve.Evaluate(distance); - //do not consider lockbreak factor from active ecm here! - //do not consider chaff here - - if (signature > minDetectSig) + // detected by radar + if (myWpnManager != null) { - // detected by radar - if (myWpnManager != null) - { - BDATargetManager.ReportVessel(loadedvessels.Current, myWpnManager); - } - - // report scanned targets only - radar.ReceiveContactData(new TargetSignatureData(loadedvessels.Current, signature), false); + BDATargetManager.ReportVessel(loadedvessels.Current, myWpnManager, true); } + + // report scanned targets only + radar.ReceiveContactData(new TargetSignatureData(loadedvessels.Current, signature), false); } // our radar ping can be received at a higher range than we can detect, according to RWR range ping factor: @@ -1160,8 +1185,9 @@ public static bool RadarUpdateLockTrack(Ray ray, Vector3 predictedPos, float fov // get vessel's radar signature TargetInfo ti = GetVesselRadarSignature(lockedVessel); float signature = ti.radarModifiedSignature; - signature *= GetRadarGroundClutterModifier(radar, radar.referenceTransform, ray.origin, lockedVessel.CoM, ti); + signature *= GetRadarGroundClutterModifier(radar.radarGroundClutterFactor, radar.referenceTransform, ray.origin, lockedVessel.CoM, ti); signature *= ti.radarLockbreakFactor; //multiply lockbreak factor from active ecm + if (radar.weaponManager is not null) signature *= GetStandoffJammingModifier(radar.vessel, radar.weaponManager.Team, ray.origin, lockedVessel, signature); //do not multiply chaff factor here // evaluate range @@ -1171,7 +1197,7 @@ public static bool RadarUpdateLockTrack(Ray ray, Vector3 predictedPos, float fov //evaluate if we can detect such a signature at that range float minTrackSig = radar.radarLockTrackCurve.Evaluate(distance); - if (signature > minTrackSig) + if ((signature >= minTrackSig) && (RadarCanDetect(radar, signature, distance))) { // can be tracked radar.ReceiveContactData(new TargetSignatureData(lockedVessel, signature), locked); @@ -1195,6 +1221,111 @@ public static bool RadarUpdateLockTrack(Ray ray, Vector3 predictedPos, float fov return false; } } + /// + /// Main scanning and locking method called from ModuleIRST. + /// scanning both for omnidirectional and boresight scans. + /// + public static bool IRSTUpdateScan(MissileFire myWpnManager, float directionAngle, Transform referenceTransform, float fov, Vector3 position, ModuleIRST irst) + { + Vector3 forwardVector = referenceTransform.forward; + Vector3 upVector = referenceTransform.up; + Vector3 lookDirection = Quaternion.AngleAxis(directionAngle, upVector) * forwardVector; + TargetSignatureData finalData = TargetSignatureData.noTarget; + Tuple IRSig; //heat value + // guard clauses + if (!myWpnManager || !myWpnManager.vessel || !irst) + return false; + + using (var loadedvessels = BDATargetManager.LoadedVessels.GetEnumerator()) + while (loadedvessels.MoveNext()) + { + // ignore null, unloaded and self + if (loadedvessels.Current == null || !loadedvessels.Current.loaded) continue; + if (loadedvessels.Current == myWpnManager.vessel) continue; + if (loadedvessels.Current.vesselType == VesselType.Debris) continue; + + // ignore too close ones + if ((loadedvessels.Current.transform.position - position).sqrMagnitude < RADAR_IGNORE_DISTANCE_SQR) + continue; + + Vector3 vesselDirection = (loadedvessels.Current.CoM - position).ProjectOnPlanePreNormalized(upVector); + float angle = Vector3.Angle(vesselDirection, lookDirection); + if (angle < fov / 2f) + { + // ignore when blocked by terrain + if (TerrainCheck(referenceTransform.position, loadedvessels.Current.transform.position)) + continue; + + // get vessel's heat signature + TargetInfo tInfo = loadedvessels.Current.gameObject.GetComponent(); + if (tInfo == null) + { + tInfo = loadedvessels.Current.gameObject.AddComponent(); + } + + IRSig = BDATargetManager.GetVesselHeatSignature(loadedvessels.Current, irst.referenceTransform.position, 1f, irst.TempSensitivityCurve); + float signature = IRSig.Item1 * (irst.boresightScan ? Mathf.Clamp01(15 / angle) : 1); + //signature *= (1400 * 1400) / Mathf.Clamp((loadedvessels.Current.CoM - referenceTransform.position).sqrMagnitude, 90000, 36000000); //300 to 6000m - clamping sig past 6km; Commenting out as it makes tuning detection curves much easier + + signature *= Mathf.Clamp(Vector3.Angle(loadedvessels.Current.transform.position - referenceTransform.position, -VectorUtils.GetUpDirection(referenceTransform.position)) / 90, 0.5f, 1.5f); + //ground will mask thermal sig + signature *= (GetRadarGroundClutterModifier(irst.GroundClutterFactor, irst.referenceTransform, position, loadedvessels.Current.CoM, tInfo) * (tInfo.isSplashed ? 12 : 1)); + //cold ocean on the other hand... + + // evaluate range + float distance = (loadedvessels.Current.CoM - position).magnitude / 1000f; //TODO: Performance! better if we could switch to sqrMagnitude... + + BDATargetManager.ClearRadarReport(loadedvessels.Current, myWpnManager); + + //evaluate if we can detect such a signature at that range + float attenuationFactor = ((float)FlightGlobals.getAtmDensity(FlightGlobals.getStaticPressure(irst.referenceTransform.position), FlightGlobals.getExternalTemperature(irst.referenceTransform.position))) + + ((float)FlightGlobals.getAtmDensity(FlightGlobals.getStaticPressure(loadedvessels.Current.CoM), FlightGlobals.getExternalTemperature(loadedvessels.Current.CoM) / 2)); + + if (distance > irst.irstMinDistanceDetect && distance < (irst.irstMaxDistanceDetect * irst.atmAttenuationCurve.Evaluate(attenuationFactor))) + { + //evaluate if we can detect or lock such a signature at that range + float minDetectSig = irst.DetectionCurve.Evaluate(distance / attenuationFactor); + + if (signature >= minDetectSig) + { + // detected by irst + if (myWpnManager != null) + { + BDATargetManager.ReportVessel(loadedvessels.Current, myWpnManager, true); + } + irst.ReceiveContactData(new TargetSignatureData(loadedvessels.Current, signature), signature); + if (BDArmorySettings.DEBUG_RADAR) Debug.Log("[IRSTdebugging] sent data to IRST for " + loadedvessels.Current.GetName() + "'s thermalSig"); + } + } + } + } + return false; + } + + /// + /// Returns whether the radar can detect the target, including jamming effects + /// + public static bool RadarCanDetect(ModuleRadar radar, float signature, float distance) + { + bool detected = false; + // float distance already in km + + //evaluate if we can detect such a signature at that range + if ((distance > radar.radarMinDistanceDetect) && (distance < radar.radarMaxDistanceDetect)) + { + //evaluate if we can detect or lock such a signature at that range + float minDetectSig = radar.radarDetectionCurve.Evaluate(distance); + //do not consider lockbreak factor from active ecm here! + //do not consider chaff here + + if (signature >= minDetectSig) + { + detected = true; + } + } + + return detected; + } /// /// Scans for targets in direction with field of view. @@ -1212,6 +1343,7 @@ public static ViewScanResults GuardScanInDirection(MissileFire myWpnManager, Tra foundAGM = false, firingAtMe = false, missDistance = float.MaxValue, + missDeviation = float.MaxValue, threatVessel = null, threatWeaponManager = null, incomingMissiles = new List() @@ -1235,19 +1367,18 @@ public static ViewScanResults GuardScanInDirection(MissileFire myWpnManager, Tra if (loadedvessels.Current == null || !loadedvessels.Current.loaded || VesselModuleRegistry.ignoredVesselTypes.Contains(loadedvessels.Current.vesselType)) continue; if (loadedvessels.Current == myWpnManager.vessel) continue; //ignore self - Vector3 vesselProjectedDirection = Vector3.ProjectOnPlane(loadedvessels.Current.transform.position - position, upVector); + Vector3 vesselProjectedDirection = (loadedvessels.Current.transform.position - position).ProjectOnPlanePreNormalized(upVector); Vector3 vesselDirection = loadedvessels.Current.transform.position - position; - float vesselDistance = (loadedvessels.Current.transform.position - position).sqrMagnitude; - if (vesselDistance < maxDistance * maxDistance && Vector3.Angle(vesselProjectedDirection, lookDirection) < fov / 2f && Vector3.Angle(loadedvessels.Current.transform.position - position, -myWpnManager.transform.forward) < myWpnManager.guardAngle / 2f) - { + float vesselDistanceSqr = (loadedvessels.Current.transform.position - position).sqrMagnitude; + //BDATargetManager.ClearRadarReport(loadedvessels.Current, myWpnManager); //reset radar contact status + if (vesselDistanceSqr < maxDistance * maxDistance && Vector3.Angle(vesselProjectedDirection, lookDirection) < fov / 2f) // && Vector3.Angle(loadedvessels.Current.transform.position - position, -myWpnManager.transform.forward) < myWpnManager.guardAngle / 2f) //WM facing direction? that s going to cause issues for any that aren't mounted pointing forward if guardAngle < 360; check combatSeat forward vector + { if (TerrainCheck(referenceTransform.position, loadedvessels.Current.transform.position)) { continue; //blocked by terrain } - BDATargetManager.ReportVessel(loadedvessels.Current, myWpnManager); - TargetInfo tInfo; if ((tInfo = loadedvessels.Current.gameObject.GetComponent())) { @@ -1257,14 +1388,20 @@ public static ViewScanResults GuardScanInDirection(MissileFire myWpnManager, Tra if (missileBase != null) { if (missileBase.SourceVessel == myWpnManager.vessel) continue; // ignore missiles we've fired - + if (BDArmorySettings.VARIABLE_MISSILE_VISIBILITY) + { + //thrusting missiles at full range, cruising missiles at 3/4ths range, coasting missiles at 1/3rd range? + //or have be hard cutoffs, e.g. 5km/4km/2.5km, etc? + float sightDistance = maxDistance * (missileBase.MissileState == MissileBase.MissileStates.Boost ? 1 : (missileBase.MissileState == MissileBase.MissileStates.Cruise ? 0.75f : 0.33f)); + if (vesselDistanceSqr > sightDistance * sightDistance) continue; //missile outside of modified visibility range, disregard + } if (MissileIsThreat(missileBase, myWpnManager)) { results.incomingMissiles.Add(new IncomingMissile { guidanceType = missileBase.TargetingMode, distance = Vector3.Distance(missileBase.part.transform.position, myWpnManager.part.transform.position), - time = AIUtils.ClosestTimeToCPA(missileBase.vessel, myWpnManager.vessel, 16f), + time = AIUtils.TimeToCPA(missileBase.vessel, myWpnManager.vessel, 16f), position = missileBase.transform.position, vessel = missileBase.vessel, weaponManager = missileBase.SourceVessel != null ? VesselModuleRegistry.GetModule(missileBase.SourceVessel) : null, @@ -1280,8 +1417,8 @@ public static ViewScanResults GuardScanInDirection(MissileFire myWpnManager, Tra case MissileBase.TargetingModes.Laser: results.foundAGM = true; break; - case MissileBase.TargetingModes.AntiRad: - results.foundAntiRadiationMissile = true; + case MissileBase.TargetingModes.AntiRad: //How does one differentiate between a passive IR sensor and a passive AR sensor? + results.foundAntiRadiationMissile = true; //admittedly, combining the two would result in launching flares at ARMs and turning off radar when having incoming heaters... break; } } @@ -1294,6 +1431,7 @@ public static ViewScanResults GuardScanInDirection(MissileFire myWpnManager, Tra } else { + if (vesselDistanceSqr > myWpnManager.guardRange * myWpnManager.guardRange) continue; using (var weapon = VesselModuleRegistry.GetModules(loadedvessels.Current).GetEnumerator()) while (weapon.MoveNext()) { @@ -1310,11 +1448,13 @@ public static ViewScanResults GuardScanInDirection(MissileFire myWpnManager, Tra results.threatVessel = weapon.Current.vessel; results.threatWeaponManager = weapon.Current.weaponManager; results.missDistance = missDistance; + results.missDeviation = (weapon.Current.fireTransforms[0].position - myWpnManager.vessel.transform.position).magnitude * weapon.Current.maxDeviation / 2f * Mathf.Deg2Rad; // y = x*tan(θ), expansion of tan(θ) is θ + O(θ^3). } } } } } + BDATargetManager.ReportVessel(loadedvessels.Current, myWpnManager); } } // Sort incoming missiles by time @@ -1348,7 +1488,7 @@ public static bool MissileIsThreat(MissileBase missile, MissileFire mf, bool thr var missileBlastRadiusSqr = 3f * missile.GetBlastRadius(); missileBlastRadiusSqr *= missileBlastRadiusSqr; - return (missile.HasFired && missile.TimeIndex > 1f && approaching && + return (missile.HasFired && missile.MissileState > MissileBase.MissileStates.Drop && approaching && ( (missile.TargetPosition - (mf.vessel.CoM + (mf.vessel.Velocity() * Time.fixedDeltaTime))).sqrMagnitude < missileBlastRadiusSqr || // Target position is within blast radius of missile. mf.vessel.PredictClosestApproachSqrSeparation(missile.vessel, mf.cmThreshold) < missileBlastRadiusSqr || // Closest approach is within blast radius of missile. @@ -1385,7 +1525,7 @@ public static bool MissileIsThreat(MissileBase missile, MissileFire mf, bool thr return false; } - private static float MissDistance(ModuleWeapon threatWeapon, Vessel self) // Returns how far away bullets from enemy are from craft in meters + public static float MissDistance(ModuleWeapon threatWeapon, Vessel self) // Returns how far away bullets from enemy are from craft in meters { Transform fireTransform = threatWeapon.fireTransforms[0]; // If we're out of range, then it's not a threat. @@ -1395,7 +1535,7 @@ private static float MissDistance(ModuleWeapon threatWeapon, Vessel self) // Ret float targetCosAngle = threatWeapon.FiringSolutionVector != null ? Vector3.Dot(aimDirection, (Vector3)threatWeapon.FiringSolutionVector) : Vector3.Dot(aimDirection, (self.vesselTransform.position - fireTransform.position).normalized); // Find vertical component of aiming angle - float angleThreat = targetCosAngle < 0 ? float.MaxValue : Mathf.Sqrt(Mathf.Max(0f, 1f - targetCosAngle * targetCosAngle)); // Treat angles beyond 90 degrees as not a threat + float angleThreat = targetCosAngle < 0 ? float.MaxValue : BDAMath.Sqrt(Mathf.Max(0f, 1f - targetCosAngle * targetCosAngle)); // Treat angles beyond 90 degrees as not a threat // Calculate distance between incoming threat position and its aimpoint (or self position) float distanceThreat = !threatWeapon.finalAimTarget.IsZero() ? Vector3.Magnitude(threatWeapon.finalAimTarget - fireTransform.position) : Vector3.Magnitude(self.vesselTransform.position - fireTransform.position); @@ -1407,7 +1547,7 @@ private static float MissDistance(ModuleWeapon threatWeapon, Vessel self) // Ret /// /// Helper method: check if line intersects terrain /// - public static bool TerrainCheck(Vector3 start, Vector3 end) + public static bool TerrainCheck(Vector3 start, Vector3 end) { if (!BDArmorySettings.IGNORE_TERRAIN_CHECK) { @@ -1425,12 +1565,20 @@ public static Vector2 WorldToRadar(Vector3 worldPosition, Transform referenceTra float scale = maxDistance / (radarRect.height / 2); Vector3 localPosition = referenceTransform.InverseTransformPoint(worldPosition); localPosition.y = 0; - Vector2 radarPos = new Vector2((radarRect.width / 2) + (localPosition.x / scale), ((radarRect.height / 2) - (localPosition.z / scale))); - return radarPos; + if (BDArmorySettings.LOGARITHMIC_RADAR_DISPLAY) + { + scale = Mathf.Log(localPosition.magnitude + 1) / Mathf.Log(maxDistance + 1); + localPosition = localPosition.normalized * scale * scale; // Log^2 gives a nicer curve than log. + return new Vector2(radarRect.width * (1 + localPosition.x) / 2, radarRect.height * (1 - localPosition.z) / 2); + } + else + { + return new Vector2((radarRect.width / 2) + (localPosition.x / scale), ((radarRect.height / 2) - (localPosition.z / scale))); + } } /// - /// Helper method: map a position onto the radar display (for non-onmi radars) + /// Helper method: map a position onto the radar display (for non-omni radars) /// public static Vector2 WorldToRadarRadial(Vector3 worldPosition, Transform referenceTransform, Rect radarRect, float maxDistance, float maxAngle) { @@ -1442,9 +1590,19 @@ public static Vector2 WorldToRadarRadial(Vector3 worldPosition, Transform refere float angle = Vector3.Angle(localPosition, Vector3.forward); if (localPosition.x < 0) angle = -angle; float xPos = (radarRect.width / 2) + ((angle / maxAngle) * radarRect.width / 2); - float yPos = radarRect.height - (new Vector2(localPosition.x, localPosition.z)).magnitude / scale; + float yPos = radarRect.height; + + if (BDArmorySettings.LOGARITHMIC_RADAR_DISPLAY) + { + scale = Mathf.Log(localPosition.magnitude + 1) / Mathf.Log(maxDistance + 1); + yPos -= radarRect.height * scale * scale; + } + else + { + yPos -= localPosition.magnitude / scale; + } Vector2 radarPos = new Vector2(xPos, yPos); return radarPos; } } -} +} \ No newline at end of file diff --git a/BDArmory/Modules/RadarWarningReceiver.cs b/BDArmory/Radar/RadarWarningReceiver.cs similarity index 88% rename from BDArmory/Modules/RadarWarningReceiver.cs rename to BDArmory/Radar/RadarWarningReceiver.cs index 43527d365..b5f74f70b 100644 --- a/BDArmory/Modules/RadarWarningReceiver.cs +++ b/BDArmory/Radar/RadarWarningReceiver.cs @@ -1,13 +1,15 @@ using System.Collections; using System.Collections.Generic; -using BDArmory.Core; -using BDArmory.Misc; +using UnityEngine; + +using BDArmory.Control; using BDArmory.Radar; +using BDArmory.Settings; using BDArmory.Targeting; using BDArmory.UI; -using UnityEngine; +using BDArmory.Utils; -namespace BDArmory.Modules +namespace BDArmory.Radar { public class RadarWarningReceiver : PartModule { @@ -15,7 +17,7 @@ public class RadarWarningReceiver : PartModule public static event RadarPing OnRadarPing; - public delegate void MissileLaunchWarning(Vector3 source, Vector3 direction); + public delegate void MissileLaunchWarning(Vector3 source, Vector3 direction, bool radar); public static event MissileLaunchWarning OnMissileLaunch; @@ -30,15 +32,18 @@ public enum RWRThreatTypes Detection = 5, Sonar = 6, Torpedo = 7, - TorpedoLock = 8 + TorpedoLock = 8, + Jamming = 9 } - string[] iconLabels = new string[] { "S", "F", "A", "M", "M", "D", "So", "T", "T" }; + string[] iconLabels = new string[] { "S", "F", "A", "M", "M", "D", "So", "T", "T", "J" }; public MissileFire weaponManager; // This field may not need to be persistent. It was combining display with active RWR status. [KSPField(isPersistant = true)] public bool rwrEnabled; + //for if the RWR should detect everything, or only be able to detect radar sources + [KSPField(isPersistant = true)] public bool omniDetection = true; // This field was added to separate RWR active status from the display of the RWR. the RWR should be running all the time... public bool displayRWR = false; @@ -105,11 +110,11 @@ Transform referenceTransform public override void OnAwake() { - radarPingSound = GameDatabase.Instance.GetAudioClip("BDArmory/Sounds/rwrPing"); - missileLockSound = GameDatabase.Instance.GetAudioClip("BDArmory/Sounds/rwrMissileLock"); - missileLaunchSound = GameDatabase.Instance.GetAudioClip("BDArmory/Sounds/mLaunchWarning"); - sonarPing = GameDatabase.Instance.GetAudioClip("BDArmory/Sounds/rwr_sonarping"); - torpedoPing = GameDatabase.Instance.GetAudioClip("BDArmory/Sounds/rwr_torpedoping"); + radarPingSound = SoundUtils.GetAudioClip("BDArmory/Sounds/rwrPing"); + missileLockSound = SoundUtils.GetAudioClip("BDArmory/Sounds/rwrMissileLock"); + missileLaunchSound = SoundUtils.GetAudioClip("BDArmory/Sounds/mLaunchWarning"); + sonarPing = SoundUtils.GetAudioClip("BDArmory/Sounds/rwr_sonarping"); + torpedoPing = SoundUtils.GetAudioClip("BDArmory/Sounds/rwr_torpedoping"); } public override void OnStart(StartState state) @@ -193,25 +198,26 @@ void OnDestroy() IEnumerator PingLifeRoutine(int index, float lifeTime) { - yield return new WaitForSeconds(Mathf.Clamp(lifeTime - 0.04f, minPingInterval, lifeTime)); + yield return new WaitForSecondsFixed(Mathf.Clamp(lifeTime - 0.04f, minPingInterval, lifeTime)); pingsData[index] = TargetSignatureData.noTarget; } IEnumerator LaunchWarningRoutine(TargetSignatureData data) { launchWarnings.Add(data); - yield return new WaitForSeconds(2); + yield return new WaitForSecondsFixed(2); launchWarnings.Remove(data); } - void ReceiveLaunchWarning(Vector3 source, Vector3 direction) + void ReceiveLaunchWarning(Vector3 source, Vector3 direction, bool radar) { if (referenceTransform == null) return; if (part == null || !part.isActiveAndEnabled) return; if (weaponManager == null) return; + if (!omniDetection && !radar) return; float sqrDist = (part.transform.position - source).sqrMagnitude; - if ((weaponManager && weaponManager.guardMode) && (sqrDist > (weaponManager.guardRange * weaponManager.guardRange))) return; + //if ((weaponManager && weaponManager.guardMode) && (sqrDist > (weaponManager.guardRange * weaponManager.guardRange))) return; //doesn't this clamp the RWR to visual view range, not radar/RWR range? if (sqrDist < BDArmorySettings.MAX_ENGAGEMENT_RANGE * BDArmorySettings.MAX_ENGAGEMENT_RANGE && sqrDist > 10000f && Vector3.Angle(direction, part.transform.position - source) < 15f) { StartCoroutine( @@ -222,15 +228,16 @@ void ReceiveLaunchWarning(Vector3 source, Vector3 direction) if (weaponManager && weaponManager.guardMode) { - weaponManager.FireAllCountermeasures(Random.Range(1, 2)); // Was 2-4, but we don't want to take too long doing this initial dump before other routines kick in + //weaponManager.FireAllCountermeasures(Random.Range(1, 2)); // Was 2-4, but we don't want to take too long doing this initial dump before other routines kick in weaponManager.incomingThreatPosition = source; + weaponManager.missileIsIncoming = true; } } } void ReceivePing(Vessel v, Vector3 source, RWRThreatTypes type, float persistTime) { - if (v == null) return; + if (v == null || v.packed || !v.loaded || !v.isActiveAndEnabled) return; if (referenceTransform == null) return; if (weaponManager == null) return; @@ -257,6 +264,7 @@ void ReceivePing(Vessel v, Vector3 source, RWRThreatTypes type, float persistTim if (weaponManager && weaponManager.guardMode) { weaponManager.FireChaff(); + weaponManager.missileIsIncoming = true; // TODO: if torpedo inbound, also fire accoustic decoys (not yet implemented...) } } @@ -266,7 +274,7 @@ void ReceivePing(Vessel v, Vector3 source, RWRThreatTypes type, float persistTim { if (pingsData[i].exists && ((Vector2)pingsData[i].position - - RadarUtils.WorldToRadar(source, referenceTransform, RwrDisplayRect, rwrDisplayRange)).sqrMagnitude < 900f) //prevent ping spam + RadarUtils.WorldToRadar(source, referenceTransform, RwrDisplayRect, rwrDisplayRange)).sqrMagnitude < (BDArmorySettings.LOGARITHMIC_RADAR_DISPLAY ? 100f : 900f)) //prevent ping spam { break; } @@ -286,6 +294,10 @@ void ReceivePing(Vessel v, Vector3 source, RWRThreatTypes type, float persistTim RadarUtils.WorldToRadar(source, referenceTransform, RwrDisplayRect, rwrDisplayRange), Vector3.zero, true, (float)type); // HACK! Evil misuse of signalstrength for the threat type! pingWorldPositions[openIndex] = source; //FIXME source is improperly defined + if (weaponManager.hasAntiRadiationOrdinance) + { + BDATargetManager.ReportVessel(AIUtils.VesselClosestTo(source), weaponManager); // Report RWR ping as target for anti-rads + } //MissileFire RWR-vessel checks are all (RWR ping position - guardtarget.CoM).Magnitude < 20*20?, could we simplify the more complex vessel aquistion function used here? StartCoroutine(PingLifeRoutine(openIndex, persistTime)); PlayWarningSound(type, (source - vessel.transform.position).sqrMagnitude); @@ -358,7 +370,7 @@ void OnGUI() } BDArmorySetup.WindowRectRwr = GUI.Window(94353, BDArmorySetup.WindowRectRwr, WindowRwr, "Radar Warning Receiver", GUI.skin.window); - BDGUIUtils.UseMouseEventInRect(RwrDisplayRect); + GUIUtils.UseMouseEventInRect(RwrDisplayRect); } internal void WindowRwr(int windowID) @@ -410,7 +422,7 @@ internal void WindowRwr(int windowID) // Resizing code block. RWRresizeRect = new Rect(BDArmorySetup.WindowRectRwr.width - 18, BDArmorySetup.WindowRectRwr.height - 18, 16, 16); - GUI.DrawTexture(RWRresizeRect, Utils.resizeTexture, ScaleMode.StretchToFill, true); + GUI.DrawTexture(RWRresizeRect, GUIUtils.resizeTexture, ScaleMode.StretchToFill, true); if (Event.current.type == EventType.MouseDown && RWRresizeRect.Contains(Event.current.mousePosition)) { resizingWindow = true; @@ -427,7 +439,7 @@ internal void WindowRwr(int windowID) } // End Resizing code. - BDGUIUtils.RepositionWindow(ref BDArmorySetup.WindowRectRwr); + GUIUtils.RepositionWindow(ref BDArmorySetup.WindowRectRwr); } internal static void UpdateRWRScale(float diff) @@ -467,9 +479,9 @@ public static void PingRWR(Ray ray, float fov, RWRThreatTypes type, float persis } } - public static void WarnMissileLaunch(Vector3 source, Vector3 direction) + public static void WarnMissileLaunch(Vector3 source, Vector3 direction, bool radarMissile) { - OnMissileLaunch?.Invoke(source, direction); + OnMissileLaunch?.Invoke(source, direction, radarMissile); } } } diff --git a/BDArmory/Radar/VesselRadarData.cs b/BDArmory/Radar/VesselRadarData.cs index b46e18957..72f6becba 100644 --- a/BDArmory/Radar/VesselRadarData.cs +++ b/BDArmory/Radar/VesselRadarData.cs @@ -1,17 +1,23 @@ using System.Collections; using System.Collections.Generic; -using BDArmory.Core; -using BDArmory.Misc; -using BDArmory.Modules; +using UnityEngine; + +using BDArmory.Competition; +using BDArmory.Control; +using BDArmory.Extensions; +using BDArmory.Settings; using BDArmory.Targeting; using BDArmory.UI; -using UnityEngine; +using BDArmory.Utils; +using BDArmory.Weapons; +using BDArmory.Weapons.Missiles; namespace BDArmory.Radar { public class VesselRadarData : MonoBehaviour { private List availableRadars; + private List availableIRSTs; private List externalRadars; private List externalVRDs; private float _maxRadarRange = 0; @@ -30,6 +36,13 @@ public int radarCount get { return rCount; } } + private int iCount; + + public int irstCount + { + get { return iCount; } + } + public bool guiEnabled { get { return drawGUI; } @@ -69,6 +82,8 @@ public bool guiEnabled private static readonly Texture2D scanTexture = GameDatabase.Instance.GetTexture(BDArmorySetup.textureDir + "omniRadarScanTexture", false); + private static readonly Texture2D IRscanTexture = GameDatabase.Instance.GetTexture(BDArmorySetup.textureDir + "omniIRSTScanTexture", + false); private static readonly Texture2D lockIcon = GameDatabase.Instance.GetTexture(BDArmorySetup.textureDir + "lockedRadarIcon", false); @@ -81,6 +96,12 @@ public bool guiEnabled private static readonly Texture2D friendlyContactIcon = GameDatabase.Instance.GetTexture(BDArmorySetup.textureDir + "friendlyContactIcon", false); + private static readonly Texture2D irContactIcon = GameDatabase.Instance.GetTexture(BDArmorySetup.textureDir + "IRContactIcon", + false); + + private static readonly Texture2D friendlyIRContactIcon = + GameDatabase.Instance.GetTexture(BDArmorySetup.textureDir + "friendlyIRContactIcon", false); + private GUIStyle distanceStyle; private GUIStyle lockStyle; private GUIStyle radarTopStyle; @@ -113,6 +134,7 @@ public bool guiEnabled //TargetSignatureData[] contacts = new TargetSignatureData[30]; private List displayedTargets; + private List displayedIRTargets; public bool locked; private int activeLockedTargetIndex; private List lockedTargetIndexes; @@ -134,6 +156,21 @@ public RadarDisplayData lockedTargetData get { return displayedTargets[lockedTargetIndexes[activeLockedTargetIndex]]; } } + public TargetSignatureData activeIRTarget() + { + TargetSignatureData data; + for (int i = 0; i < displayedIRTargets.Count; i++) + { + if (displayedIRTargets[i].vessel == weaponManager.currentTarget) + { + data = displayedIRTargets[i].targetData; + return data; + } + } + data = TargetSignatureData.noTarget; + return data; + } + //turret slaving public bool slaveTurrets; @@ -168,6 +205,26 @@ public void RemoveRadar(ModuleRadar mr) rangeCapabilityDirty = true; } + public void AddIRST(ModuleIRST mi) + { + if (availableIRSTs.Contains(mi)) + { + return; + } + + availableIRSTs.Add(mi); + iCount = availableIRSTs.Count; + rangeCapabilityDirty = true; + } + + public void RemoveIRST(ModuleIRST mi) + { + availableIRSTs.Remove(mi); + iCount = availableIRSTs.Count; + RemoveDataFromIRST(mi); + rangeCapabilityDirty = true; + } + public bool linkCapabilityDirty; public bool rangeCapabilityDirty; public bool radarsReady; @@ -175,6 +232,7 @@ public void RemoveRadar(ModuleRadar mr) private void Awake() { availableRadars = new List(); + availableIRSTs = new List(); externalRadars = new List(); myVessel = GetComponent(); lockedTargetIndexes = new List(); @@ -206,6 +264,7 @@ private void Awake() vesselReferenceTransform.localScale = Vector3.one; displayedTargets = new List(); + displayedIRTargets = new List(); externalVRDs = new List(); waitingForVessels = new List(); @@ -256,12 +315,7 @@ private void Start() private IEnumerator StartupRoutine() { - while (!FlightGlobals.ready || vessel.packed) - { - yield return null; - } - - yield return new WaitForFixedUpdate(); + yield return new WaitWhile(() => !FlightGlobals.ready || (vessel is not null && (vessel.packed || !vessel.loaded))); yield return new WaitForFixedUpdate(); radarsReady = true; } @@ -341,15 +395,30 @@ private void OnChangeTeam(MissileFire wm, BDTeam team) private void UpdateRangeCapability() { _maxRadarRange = 0; - List.Enumerator rad = availableRadars.GetEnumerator(); - while (rad.MoveNext()) + if (availableRadars.Count > 0) { - if (rad.Current == null) continue; - float maxRange = rad.Current.radarDetectionCurve.maxTime * 1000; - if (rad.Current.vessel != vessel || !(maxRange > 0)) continue; - if (maxRange > _maxRadarRange) _maxRadarRange = maxRange; + List.Enumerator rad = availableRadars.GetEnumerator(); + while (rad.MoveNext()) + { + if (rad.Current == null) continue; + float maxRange = rad.Current.radarDetectionCurve.maxTime * 1000; + if ((rad.Current.vessel != vessel && !externalRadars.Contains(rad.Current)) || !(maxRange > 0)) continue; + if (maxRange > _maxRadarRange) _maxRadarRange = maxRange; + } + rad.Dispose(); + } + else if (availableIRSTs.Count > 0) + { + List.Enumerator irst = availableIRSTs.GetEnumerator(); + while (irst.MoveNext()) + { + if (irst.Current == null) continue; + float maxRange = irst.Current.DetectionCurve.maxTime * 1000; + if (irst.Current.vessel != vessel || !(maxRange > 0)) continue; + if (maxRange > _maxRadarRange) _maxRadarRange = maxRange; + } + irst.Dispose(); } - rad.Dispose(); // Now rebuild range display array List newArray = new List(); for (int x = 0; x < baseIncrements.Length; x++) @@ -371,7 +440,7 @@ private void UpdateDataLinkCapability() while (rad.MoveNext()) { if (rad.Current == null) continue; - if (rad.Current.vessel == vessel && rad.Current.canRecieveRadarData) + if (rad.Current.vessel == vessel && rad.Current.canReceiveRadarData) { canReceiveRadarData = true; } @@ -454,7 +523,31 @@ private void RemoveDisconnectedRadars() rrad.Dispose(); rCount = availableRadars.Count; - RemoveEmptyVRDs(); + availableIRSTs.RemoveAll(r => r == null); + List IRSTsToRemove = new List(); + List.Enumerator irst = availableIRSTs.GetEnumerator(); + while (irst.MoveNext()) + { + if (irst.Current == null) continue; + if (!irst.Current.irstEnabled || irst.Current.vessel != vessel) + { + IRSTsToRemove.Add(irst.Current); + } + else if (!irst.Current.weaponManager || (weaponManager && irst.Current.weaponManager.Team != weaponManager.Team)) + { + IRSTsToRemove.Add(irst.Current); + } + } + irst.Dispose(); + + List.Enumerator rirs = IRSTsToRemove.GetEnumerator(); + while (rirs.MoveNext()) + { + if (rirs.Current == null) continue; + RemoveIRST(rirs.Current); + } + rirs.Dispose(); + iCount = availableIRSTs.Count; } public void UpdateLockedTargets() @@ -488,6 +581,17 @@ private void UpdateSlaveData() } private void Update() + { + if (radarCount + irstCount > 0) + { + UpdateInputs(); + } + + drawGUI = (HighLogic.LoadedSceneIsFlight && FlightGlobals.ready && !vessel.packed && rCount + iCount > 0 && + vessel.isActiveVessel && BDArmorySetup.GAME_UI_ENABLED && !MapView.MapIsEnabled); + } + + void FixedUpdate() { if (!vessel) { @@ -497,7 +601,7 @@ private void Update() UpdateReferenceTransform(); - if (radarCount > 0) + if (radarCount + irstCount > 0) { //vesselReferenceTransform.parent = linkedRadars[0].transform; vesselReferenceTransform.localScale = Vector3.one; @@ -505,12 +609,10 @@ private void Update() vesselReferenceTransform.rotation = Quaternion.LookRotation(vessel.LandedOrSplashed ? VectorUtils.GetNorthVector(vessel.transform.position, vessel.mainBody) : - Vector3.ProjectOnPlane(vessel.transform.up, vessel.upAxis), vessel.upAxis); + vessel.transform.up.ProjectOnPlanePreNormalized(vessel.upAxis), vessel.upAxis); CleanDisplayedContacts(); - UpdateInputs(); - UpdateSlaveData(); } else @@ -533,10 +635,10 @@ private void Update() rangeCapabilityDirty = false; } - drawGUI = (HighLogic.LoadedSceneIsFlight && FlightGlobals.ready && !vessel.packed && rCount > 0 && + drawGUI = (HighLogic.LoadedSceneIsFlight && FlightGlobals.ready && !vessel.packed && rCount + iCount > 0 && vessel.isActiveVessel && BDArmorySetup.GAME_UI_ENABLED && !MapView.MapIsEnabled); - if (!vessel.loaded && radarCount == 0) + if (!vessel.loaded && (radarCount + irstCount == 0)) { Destroy(this); } @@ -595,37 +697,39 @@ private bool TryLockTarget(RadarDisplayData radarTarget) ModuleRadar lockingRadar = null; //first try using the last radar to detect that target + bool acquiredLock = false; if (CheckRadarForLock(radarTarget.detectedByRadar, radarTarget)) { lockingRadar = radarTarget.detectedByRadar; + acquiredLock = (lockingRadar.TryLockTarget(radarTarget.targetData.predictedPosition, radarTarget.vessel)); } - else + if (!acquiredLock) //locks exceeded/target off scope, test if remaining radars have available locks & coveravge { - List.Enumerator radar = availableRadars.GetEnumerator(); - while (radar.MoveNext()) - { - if (radar.Current == null) continue; - if (!CheckRadarForLock(radar.Current, radarTarget)) continue; - lockingRadar = radar.Current; - break; - } - radar.Dispose(); + using (List.Enumerator radar = availableRadars.GetEnumerator()) + while (radar.MoveNext()) + { + if (radar.Current == null) continue; + if (!CheckRadarForLock(radar.Current, radarTarget)) continue; + lockingRadar = radar.Current; + if (lockingRadar.TryLockTarget(radarTarget.targetData.predictedPosition, radarTarget.vessel)) + { + acquiredLock = true; + break; + } + } } - if (lockingRadar != null) { - return lockingRadar.TryLockTarget(radarTarget.targetData.predictedPosition, radarTarget.vessel); + return acquiredLock; } - UpdateLockedTargets(); - if (this != null) // Don't trigger if the gameObject was just destroyed. - StartCoroutine(UpdateLocksAfterFrame()); + StartCoroutine(UpdateLocksAfterFrame()); return false; } private IEnumerator UpdateLocksAfterFrame() { - yield return null; + yield return new WaitForFixedUpdate(); UpdateLockedTargets(); } @@ -645,7 +749,7 @@ public void TryLockTarget(Vector3 worldPosition) public bool TryLockTarget(Vessel v) { - if (v == null) return false; + if (v == null || v.packed) return false; using (List.Enumerator displayData = displayedTargets.GetEnumerator()) while (displayData.MoveNext()) @@ -676,7 +780,8 @@ private bool CheckRadarForLock(ModuleRadar radar, RadarDisplayData radarTarget) ( radar.canLock && (!radar.locked || radar.currentLocks < radar.maxLocks) - && radarTarget.targetData.signalStrength > radar.radarLockTrackCurve.Evaluate((radarTarget.targetData.predictedPosition - radar.transform.position).magnitude / 1000f) + && RadarUtils.RadarCanDetect(radar, radarTarget.targetData.signalStrength, (radarTarget.targetData.predictedPosition - radar.transform.position).magnitude / 1000f) + && radarTarget.targetData.signalStrength >= radar.radarLockTrackCurve.Evaluate((radarTarget.targetData.predictedPosition - radar.transform.position).magnitude / 1000f) && (radar.omnidirectional || Vector3.Angle(radar.transform.up, radarTarget.targetData.predictedPosition - radar.transform.position) < @@ -699,6 +804,16 @@ private void DisableAllRadars() radar.Current.DisableRadar(); } } + var irsts = VesselModuleRegistry.GetModules(vessel); + if (irsts != null) + { + using (var irst = irsts.GetEnumerator()) + while (irst.MoveNext()) + { + if (irst.Current == null) continue; + irst.Current.DisableIRST(); + } + } } public void SlaveTurrets() @@ -743,7 +858,7 @@ private void OnGUI() for (int i = 0; i < lockedTargetIndexes.Count; i++) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_RADAR) { string label = string.Empty; if (i == activeLockedTargetIndex) @@ -758,7 +873,7 @@ private void OnGUI() { label += displayedTargets[lockedTargetIndexes[i]].vessel.vesselName; } - GUI.Label(new Rect(20, 60 + (i * 26), 800, 446), label); + GUI.Label(new Rect(20, 120 + (i * 16), 800, 26), label); } TargetSignatureData lockedTarget = displayedTargets[lockedTargetIndexes[i]].targetData; @@ -766,18 +881,18 @@ private void OnGUI() { if (weaponManager && weaponManager.Team.IsFriendly(lockedTarget.Team)) { - BDGUIUtils.DrawTextureOnWorldPos(lockedTarget.predictedPosition, + GUIUtils.DrawTextureOnWorldPos(lockedTarget.predictedPosition, BDArmorySetup.Instance.crossedGreenSquare, new Vector2(20, 20), 0); } else { - BDGUIUtils.DrawTextureOnWorldPos(lockedTarget.predictedPosition, + GUIUtils.DrawTextureOnWorldPos(lockedTarget.predictedPosition, BDArmorySetup.Instance.openGreenSquare, new Vector2(20, 20), 0); } } else { - BDGUIUtils.DrawTextureOnWorldPos(lockedTarget.predictedPosition, + GUIUtils.DrawTextureOnWorldPos(lockedTarget.predictedPosition, BDArmorySetup.Instance.greenDiamondTexture, new Vector2(17, 17), 0); } } @@ -788,7 +903,7 @@ private void OnGUI() } const string windowTitle = "Radar"; BDArmorySetup.WindowRectRadar = GUI.Window(524141, BDArmorySetup.WindowRectRadar, WindowRadar, windowTitle, GUI.skin.window); - BDGUIUtils.UseMouseEventInRect(BDArmorySetup.WindowRectRadar); + GUIUtils.UseMouseEventInRect(BDArmorySetup.WindowRectRadar); if (linkWindowOpen && canReceiveRadarData) { @@ -796,7 +911,7 @@ private void OnGUI() 16 + (numberOfAvailableLinks * linkRectEntryHeight)); LinkRadarWindow(); - BDGUIUtils.UseMouseEventInRect(linkWindowRect); + GUIUtils.UseMouseEventInRect(linkWindowRect); } } @@ -805,7 +920,7 @@ private void OnGUI() private void WindowRadar(int windowID) { GUI.DragWindow(new Rect(0, 0, BDArmorySetup.WindowRectRadar.width - 18, 30)); - if (GUI.Button(new Rect(BDArmorySetup.WindowRectRadar.width - 18, 2, 16, 16), "X", GUI.skin.button)) + if (GUI.Button(new Rect(BDArmorySetup.WindowRectRadar.width - 18, 2, 16, 16), "X", GUI.skin.button)) //this won't actually close radar GUI, just turn all radars off. This intentional? { DisableAllRadars(); BDArmorySetup.SaveConfig(); @@ -813,12 +928,12 @@ private void WindowRadar(int windowID) } if (!referenceTransform) return; + if (availableRadars.Count + availableIRSTs.Count == 0) return; //============================== GUI.BeginGroup(RadarDisplayRect); - if (availableRadars.Count == 0) return; //bool omnidirectionalDisplay = (radarCount == 1 && linkedRadars[0].omnidirectional); - float directionalFieldOfView = omniDisplay ? 0 : availableRadars[0].directionalFieldOfView; + float directionalFieldOfView = omniDisplay ? 0 : availableRadars.Count > 0 ? availableRadars[0].directionalFieldOfView : availableIRSTs[0].directionalFieldOfView; //bool linked = (radarCount > 1); Rect scanRect = new Rect(0, 0, RadarDisplayRect.width, RadarDisplayRect.height); if (omniDisplay) @@ -831,11 +946,11 @@ private void WindowRadar(int windowID) } // Range Display and control - DisplayRange(); + if (availableRadars.Count > 0 || availableIRSTs[0].irstRanging) DisplayRange(); //don't change dist for non-range capable IRSTs //my ship direction icon float directionSize = 16; - Vector3 projectedVesselFwd = Vector3.ProjectOnPlane(vessel.ReferenceTransform.up, referenceTransform.up); + Vector3 projectedVesselFwd = vessel.ReferenceTransform.up.ProjectOnPlanePreNormalized(referenceTransform.up); float dAngle = Vector3.Angle(projectedVesselFwd, referenceTransform.forward); if (referenceTransform.InverseTransformVector(vessel.ReferenceTransform.up).x < 0) { @@ -856,7 +971,7 @@ private void WindowRadar(int windowID) float currentAngle = availableRadars[i].currentAngle; float radarAngle = VectorUtils.SignedAngle(projectedVesselFwd, - Vector3.ProjectOnPlane(availableRadars[i].transform.up, referenceTransform.up), + availableRadars[i].transform.up.ProjectOnPlanePreNormalized(referenceTransform.up), referenceTransform.right); if (!canScan || availableRadars[i].vessel != vessel) continue; @@ -881,7 +996,7 @@ private void WindowRadar(int windowID) } else { - BDGUIUtils.DrawRectangle( + GUIUtils.DrawRectangle( new Rect(scanRect.x + (scanRect.width / 2) - 1, scanRect.y, 2, scanRect.height / 2), new Color(0, 1, 0, 0.35f)); } @@ -895,10 +1010,60 @@ private void WindowRadar(int windowID) Rect verticalLineRect = new Rect(scanRect.center.x - (lineWidth / 2), 0, lineWidth, scanRect.center.y); GUIUtility.RotateAroundPivot(dAngle + fovAngle + radarAngle, scanRect.center); - BDGUIUtils.DrawRectangle(verticalLineRect, new Color(0, 1, 0, 0.6f)); + GUIUtils.DrawRectangle(verticalLineRect, new Color(0, 1, 0, 0.6f)); + GUI.matrix = Matrix4x4.identity; + GUIUtility.RotateAroundPivot(dAngle - fovAngle + radarAngle, scanRect.center); + GUIUtils.DrawRectangle(verticalLineRect, new Color(0, 1, 0, 0.4f)); + GUI.matrix = Matrix4x4.identity; + } + for (int i = 0; i < iCount; i++) + { + bool canScan = availableIRSTs[i].canScan; + float currentAngle = availableIRSTs[i].currentAngle; + + float radarAngle = VectorUtils.SignedAngle(projectedVesselFwd, + availableIRSTs[i].transform.up.ProjectOnPlanePreNormalized(referenceTransform.up), + referenceTransform.right); + + if (!canScan || availableIRSTs[i].vessel != vessel) continue; + + if (!availableIRSTs[i].omnidirectional) + { + currentAngle += radarAngle + dAngle; + } + else if (!vessel.Landed) + { + Vector3 north = VectorUtils.GetNorthVector(referenceTransform.position, vessel.mainBody); + float angleFromNorth = VectorUtils.SignedAngle(north, projectedVesselFwd, + Vector3.Cross(north, vessel.upAxis)); + currentAngle += angleFromNorth; + } + + GUIUtility.RotateAroundPivot(currentAngle, new Vector2((RadarScreenSize * BDArmorySettings.RADAR_WINDOW_SCALE) / 2, (RadarScreenSize * BDArmorySettings.RADAR_WINDOW_SCALE) / 2)); + if (availableIRSTs[i].omnidirectional && irstCount == 1) + { + GUI.DrawTexture(scanRect, IRscanTexture, ScaleMode.StretchToFill, true); + } + else + { + GUIUtils.DrawRectangle( + new Rect(scanRect.x + (scanRect.width / 2) - 1, scanRect.y, 2, scanRect.height / 2), + new Color(1, 0, 0, 0.35f)); + } + GUI.matrix = Matrix4x4.identity; + + + //if linked and directional, draw FOV lines + if (availableIRSTs[i].omnidirectional) continue; + float fovAngle = availableIRSTs[i].directionalFieldOfView / 2; + float lineWidth = 2; + Rect verticalLineRect = new Rect(scanRect.center.x - (lineWidth / 2), 0, lineWidth, + scanRect.center.y); + GUIUtility.RotateAroundPivot(dAngle + fovAngle + radarAngle, scanRect.center); + GUIUtils.DrawRectangle(verticalLineRect, new Color(1, 0, 0, 0.6f)); GUI.matrix = Matrix4x4.identity; GUIUtility.RotateAroundPivot(dAngle - fovAngle + radarAngle, scanRect.center); - BDGUIUtils.DrawRectangle(verticalLineRect, new Color(0, 1, 0, 0.4f)); + GUIUtils.DrawRectangle(verticalLineRect, new Color(1, 0, 0, 0.4f)); GUI.matrix = Matrix4x4.identity; } } @@ -906,7 +1071,7 @@ private void WindowRadar(int windowID) { GUI.DrawTexture(scanRect, radialBgTexture, ScaleMode.StretchToFill, true); - DisplayRange(); + if (availableRadars.Count > 0 || availableIRSTs[0].irstRanging) DisplayRange(); //don't change dist for non-range capable IRSTs for (int i = 0; i < rCount; i++) { @@ -947,6 +1112,21 @@ private void WindowRadar(int windowID) Texture2D.whiteTexture, ScaleMode.StretchToFill, true); GUI.color = origColor; } + + for (int i = 0; i < iCount; i++) + { + bool canScan = availableIRSTs[i].canScan; + float currentAngle = availableIRSTs[i].currentAngle; + if (!canScan) continue; + float indicatorAngle = currentAngle; //locked ? lockScanAngle : currentAngle; + Vector2 scanIndicatorPos = + RadarUtils.WorldToRadarRadial( + referenceTransform.position + + (Quaternion.AngleAxis(indicatorAngle, referenceTransform.up) * referenceTransform.forward), + referenceTransform, scanRect, 5000, directionalFieldOfView / 2); + GUI.DrawTexture(new Rect(scanIndicatorPos.x - 7, scanIndicatorPos.y - 10, 14, 20), + BDArmorySetup.Instance.greenDiamondTexture, ScaleMode.StretchToFill, true); //FIXME? + } } //selector @@ -958,8 +1138,8 @@ private void WindowRadar(int windowID) Rect sLeftRect = new Rect(selectorRect.x, selectorRect.y, selectorSize / 6, selectorRect.height); Rect sRightRect = new Rect(selectorRect.x + selectorRect.width - (selectorSize / 6), selectorRect.y, selectorSize / 6, selectorRect.height); - BDGUIUtils.DrawRectangle(sLeftRect, Color.green); - BDGUIUtils.DrawRectangle(sRightRect, Color.green); + GUIUtils.DrawRectangle(sLeftRect, Color.green); + GUIUtils.DrawRectangle(sRightRect, Color.green); } //missile data @@ -980,21 +1160,27 @@ private void WindowRadar(int windowID) if (!vessel.Landed) { Vector3 localUp = vessel.ReferenceTransform.InverseTransformDirection(referenceTransform.up); - localUp = Vector3.ProjectOnPlane(localUp, Vector3.up).normalized; - float rollAngle = -Utils.SignedAngle(-Vector3.forward, localUp, Vector3.right); + localUp = localUp.ProjectOnPlanePreNormalized(Vector3.up).normalized; + float rollAngle = -BDAMath.SignedAngle(-Vector3.forward, localUp, Vector3.right); GUIUtility.RotateAroundPivot(rollAngle, scanRect.center); GUI.DrawTexture(scanRect, rollIndicatorTexture, ScaleMode.StretchToFill, true); GUI.matrix = Matrix4x4.identity; } - if (noData) + if (noData)// && iCount == 0) { - GUI.Label(RadarDisplayRect, "NO DATA\n", lockStyle); + if (iCount > 0) + DrawDisplayedIRContacts(); + else + GUI.Label(RadarDisplayRect, "NO DATA\n", lockStyle); } else { DrawDisplayedContacts(); + if (iCount > 0) + DrawDisplayedIRContacts(); } + pingPositionsDirty = false; GUI.EndGroup(); @@ -1002,9 +1188,8 @@ private void WindowRadar(int windowID) DisplayRadarControls(); // Resizing code block. - RADARresizeRect = - new Rect(BDArmorySetup.WindowRectRadar.width - 18, BDArmorySetup.WindowRectRadar.height - 19, 16, 16); - GUI.DrawTexture(RADARresizeRect, Utils.resizeTexture, ScaleMode.StretchToFill, true); + RADARresizeRect = new Rect(BDArmorySetup.WindowRectRadar.width - 18, BDArmorySetup.WindowRectRadar.height - 19, 16, 16); + GUI.DrawTexture(RADARresizeRect, GUIUtils.resizeTexture, ScaleMode.StretchToFill, true); if (Event.current.type == EventType.MouseDown && RADARresizeRect.Contains(Event.current.mousePosition)) { resizingWindow = true; @@ -1021,7 +1206,7 @@ private void WindowRadar(int windowID) } // End Resizing code. - BDGUIUtils.RepositionWindow(ref BDArmorySetup.WindowRectRadar); + GUIUtils.RepositionWindow(ref BDArmorySetup.WindowRectRadar); } internal static void UpdateRadarScale(float diff) @@ -1100,10 +1285,11 @@ private void DisplayRadarControls() { if (!locked) { - string boresightToggle = availableRadars[0].boresightScan ? "Scan" : "Boresight"; + string boresightToggle = (availableRadars.Count > 0 ? availableRadars[0].boresightScan : availableIRSTs[0].boresightScan) ? "Scan" : "Boresight"; if (GUI.Button(lockModeCycleRect, boresightToggle, GUI.skin.button)) { - availableRadars[0].boresightScan = !availableRadars[0].boresightScan; + if (availableRadars.Count > 0) availableRadars[0].boresightScan = !availableRadars[0].boresightScan; + if (availableIRSTs.Count > 0) availableIRSTs[0].boresightScan = !availableIRSTs[0].boresightScan; } } } @@ -1181,15 +1367,32 @@ private void LinkRadarWindow() GUI.EndGroup(); } + public void LinkAllRadars() + { + RefreshAvailableLinks(); + List.Enumerator v = availableExternalVRDs.GetEnumerator(); + while (v.MoveNext()) + { + if (v.Current == null) continue; + if (!v.Current.vessel || !v.Current.vessel.loaded) continue; + if (!externalVRDs.Contains(v.Current)) + LinkVRD(v.Current); + } + v.Dispose(); + } + public void RemoveDataFromRadar(ModuleRadar radar) { displayedTargets.RemoveAll(t => t.detectedByRadar == radar); UpdateLockedTargets(); } - + public void RemoveDataFromIRST(ModuleIRST irst) + { + displayedIRTargets.RemoveAll(t => t.detectedByIRST == irst); + } private void UnlinkVRD(VesselRadarData vrd) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.VesselRadarData]: Unlinking VRD: " + vrd.vessel.vesselName); + if (BDArmorySettings.DEBUG_RADAR) Debug.Log("[BDArmory.VesselRadarData]: Unlinking VRD: " + vrd.vessel.vesselName); externalVRDs.Remove(vrd); List radarsToUnlink = new List(); @@ -1209,7 +1412,7 @@ private void UnlinkVRD(VesselRadarData vrd) while (mr.MoveNext()) { if (mr.Current == null) continue; - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.VesselRadarData]: - Unlinking radar: " + mr.Current.radarName); + if (BDArmorySettings.DEBUG_RADAR) Debug.Log("[BDArmory.VesselRadarData]: - Unlinking radar: " + mr.Current.radarName); UnlinkRadar(mr.Current); } mr.Dispose(); @@ -1310,18 +1513,15 @@ private IEnumerator RecoverUnloadedLinkedVesselRoutine(string vesselID) yield break; } - yield return new WaitForSeconds(0.5f); + yield return new WaitForSecondsFixed(0.5f); } } private IEnumerator LinkVRDWhenReady(VesselRadarData vrd) { - while (!vrd.radarsReady || vrd.vessel.packed || vrd.radarCount < 1) - { - yield return null; - } + yield return new WaitWhileFixed(() => !vrd.radarsReady || (vrd.vessel is not null && (vrd.vessel.packed || !vrd.vessel.loaded)) || vrd.radarCount < 1); LinkVRD(vrd); - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.VesselRadarData]: Radar data link recovered: Local - " + vessel.vesselName + ", External - " + + if (BDArmorySettings.DEBUG_RADAR) Debug.Log("[BDArmory.VesselRadarData]: Radar data link recovered: Local - " + vessel.vesselName + ", External - " + vrd.vessel.vesselName); } @@ -1342,7 +1542,9 @@ public void UnlinkAllExternalRadars() availableRadars.RemoveAll(r => r == null); availableRadars.RemoveAll(r => r.vessel != vessel); rCount = availableRadars.Count; - + availableIRSTs.RemoveAll(r => r == null); + availableIRSTs.RemoveAll(r => r.vessel != vessel); + iCount = availableIRSTs.Count; RefreshAvailableLinks(); } @@ -1483,6 +1685,23 @@ public void AddRadarContact(ModuleRadar radar, TargetSignatureData contactData, } } + public void AddIRSTContact(ModuleIRST irst, TargetSignatureData contactData, float magnitude) + { + IRSTDisplayData rData = new IRSTDisplayData(); + rData.vessel = contactData.vessel; + + if (rData.vessel == vessel) return; + + rData.signalPersistTime = irst.signalPersistTime; + rData.detectedByIRST = irst; + rData.magnitude = magnitude; + rData.targetData = contactData; + rData.pingPosition = UpdatedPingPosition(contactData.position, irst); + displayedIRTargets.Add(rData); + + return; + } + public void TargetNext() { // activeLockedTargetIndex is the index to the list of locked targets. @@ -1634,11 +1853,23 @@ public void UnlockCurrentTarget() ModuleRadar rad = displayedTargets[lockedTargetIndexes[activeLockedTargetIndex]].detectedByRadar; rad.UnlockTargetAt(rad.currentLockIndex); } + public void UnlockSelectedTarget(Vessel vessel) + { + if (!locked) return; + + var vesselIndex = displayedTargets.FindIndex(t => t.vessel == vessel); + if (vesselIndex != -1) + { + ModuleRadar rad = displayedTargets[lockedTargetIndexes[vesselIndex]].detectedByRadar; + rad.UnlockTargetAt(rad.currentLockIndex); + } + } private void CleanDisplayedContacts() { int count = displayedTargets.Count; displayedTargets.RemoveAll(t => t.targetData.age > t.signalPersistTime * 2); + displayedIRTargets.RemoveAll(t => t.targetData.age > t.signalPersistTime * 2); if (count != displayedTargets.Count) { UpdateLockedTargets(); @@ -1659,6 +1890,20 @@ private Vector2 UpdatedPingPosition(Vector3 worldPosition, ModuleRadar radar) } } + private Vector2 UpdatedPingPosition(Vector3 worldPosition, ModuleIRST irst) + { + if (rangeIndex < 0 || rangeIndex > rIncrements.Length - 1) rangeIndex = rIncrements.Length - 1; + if (omniDisplay) + { + return RadarUtils.WorldToRadar(worldPosition, referenceTransform, RadarDisplayRect, rIncrements[rangeIndex]); + } + else + { + return RadarUtils.WorldToRadarRadial(worldPosition, referenceTransform, RadarDisplayRect, + rIncrements[rangeIndex], irst.directionalFieldOfView / 2); + } + } + private bool pingPositionsDirty = true; private void DrawDisplayedContacts() @@ -1689,8 +1934,8 @@ private void DrawDisplayedContacts() displayedTargets[i].detectedByRadar.directionalFieldOfView / 2); } - //BDGUIUtils.DrawRectangle(new Rect(pingPosition.x-(4),pingPosition.y-(4),8, 8), Color.green); - float vAngle = Vector3.Angle(Vector3.ProjectOnPlane(lockedTarget.velocity, referenceTransform.up), + //GUIUtils.DrawRectangle(new Rect(pingPosition.x-(4),pingPosition.y-(4),8, 8), Color.green); + float vAngle = Vector3.Angle(lockedTarget.velocity.ProjectOnPlanePreNormalized(referenceTransform.up), referenceTransform.forward); if (referenceTransform.InverseTransformVector(lockedTarget.velocity).x < 0) { @@ -1717,7 +1962,7 @@ private void DrawDisplayedContacts() } } - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_RADAR) { GUI.Label(new Rect(pingPosition.x + (pingSize.x / 2), pingPosition.y, 100, 24), lockedTarget.signalStrength.ToString("0.0")); @@ -1770,30 +2015,30 @@ private void DrawDisplayedContacts() float lineWidth = 2; float dlzX = RadarDisplayRect.width - dlzWidth - lineWidth; - BDGUIUtils.DrawRectangle(new Rect(dlzX, 0, dlzWidth, RadarDisplayRect.height), Color.black); + GUIUtils.DrawRectangle(new Rect(dlzX, 0, dlzWidth, RadarDisplayRect.height), Color.black); Rect maxRangeVertLineRect = new Rect(RadarDisplayRect.width - lineWidth, Mathf.Clamp(RadarDisplayRect.height - (dlz.maxLaunchRange * rangeToPixels), 0, RadarDisplayRect.height), lineWidth, Mathf.Clamp(dlz.maxLaunchRange * rangeToPixels, 0, RadarDisplayRect.height)); - BDGUIUtils.DrawRectangle(maxRangeVertLineRect, Color.green); + GUIUtils.DrawRectangle(maxRangeVertLineRect, Color.green); Rect maxRangeTickRect = new Rect(dlzX, maxRangeVertLineRect.y, dlzWidth, lineWidth); - BDGUIUtils.DrawRectangle(maxRangeTickRect, Color.green); + GUIUtils.DrawRectangle(maxRangeTickRect, Color.green); Rect minRangeTickRect = new Rect(dlzX, Mathf.Clamp(RadarDisplayRect.height - (dlz.minLaunchRange * rangeToPixels), 0, RadarDisplayRect.height), dlzWidth, lineWidth); - BDGUIUtils.DrawRectangle(minRangeTickRect, Color.green); + GUIUtils.DrawRectangle(minRangeTickRect, Color.green); Rect rTrTickRect = new Rect(dlzX, Mathf.Clamp(RadarDisplayRect.height - (dlz.rangeTr * rangeToPixels), 0, RadarDisplayRect.height), dlzWidth, lineWidth); - BDGUIUtils.DrawRectangle(rTrTickRect, Color.green); + GUIUtils.DrawRectangle(rTrTickRect, Color.green); Rect noEscapeLineRect = new Rect(dlzX, rTrTickRect.y, lineWidth, minRangeTickRect.y - rTrTickRect.y); - BDGUIUtils.DrawRectangle(noEscapeLineRect, Color.green); + GUIUtils.DrawRectangle(noEscapeLineRect, Color.green); float targetDistIconSize = 16 * BDArmorySettings.RADAR_WINDOW_SCALE; float targetDistY; @@ -1874,7 +2119,7 @@ private void DrawDisplayedContacts() lockIconSize, lockIconSize); float vAngle = Vector3.Angle( - Vector3.ProjectOnPlane(displayedTargets[i].targetData.velocity, referenceTransform.up), + displayedTargets[i].targetData.velocity.ProjectOnPlanePreNormalized(referenceTransform.up), referenceTransform.forward); if (referenceTransform.InverseTransformVector(displayedTargets[i].targetData.velocity).x < 0) { @@ -1916,7 +2161,7 @@ private void DrawDisplayedContacts() Random.Range(100, rIncrements[rangeIndex])); float bearingVariation = Mathf.Clamp( - Mathf.Pow(32000, 2) / + 1024e6f / // 32000 * 32000 (displayedTargets[i].targetData.position - transform.position).sqrMagnitude, 0, 80); jammedPosition = transform.position + @@ -1966,7 +2211,7 @@ private void DrawDisplayedContacts() if (jammed || !weaponManager.Team.IsFriendly(displayedTargets[i].targetData.Team)) { - BDGUIUtils.DrawRectangle(jammedRect, iconColor - new Color(0, 0, 0, minusAlpha)); + GUIUtils.DrawRectangle(jammedRect, iconColor - new Color(0, 0, 0, minusAlpha)); } else { @@ -1991,19 +2236,153 @@ private void DrawDisplayedContacts() TryLockTarget(displayedTargets[i]); } - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_RADAR) { GUI.Label(new Rect(pingPosition.x + (pingSize.x / 2), pingPosition.y, 100, 24), displayedTargets[i].targetData.signalStrength.ToString("0.0")); } } } - pingPositionsDirty = false; + + } + + private void DrawDisplayedIRContacts() + { + float lockIconSize = 24 * BDArmorySettings.RADAR_WINDOW_SCALE; + Vector2 Centerpoint = new Vector2((RadarScreenSize * BDArmorySettings.RADAR_WINDOW_SCALE) / 2, (RadarScreenSize * BDArmorySettings.RADAR_WINDOW_SCALE) / 2); + for (int i = 0; i < displayedIRTargets.Count; i++) + { + bool hasRadarContact = false; + if (displayedIRTargets[i].detectedByIRST.irstRanging) + { + if (displayedTargets.Count > 0) //if Radar enabled, don't display targets that have already been displayed + { + for (int r = 0; r < displayedTargets.Count; r++) + { + if (displayedIRTargets[i].targetData.targetInfo == displayedTargets[r].targetData.targetInfo) + { + hasRadarContact = true; + break; + } + } + } + } + if (!hasRadarContact) //have !radar contacts be displayed on the rim, since IRSt doesn't do ranging. + { + float minusAlpha = + (Mathf.Clamp01((Time.time - displayedIRTargets[i].targetData.timeAcquired) / + displayedIRTargets[i].signalPersistTime) * 2) - 1; + + if (pingPositionsDirty) + { + //displayedTargets[i].pingPosition = UpdatedPingPosition(displayedTargets[i].targetData.position, displayedTargets[i].detectedByRadar); + IRSTDisplayData newData = new IRSTDisplayData(); + newData.detectedByIRST = displayedIRTargets[i].detectedByIRST; + newData.magnitude = displayedIRTargets[i].magnitude; + newData.pingPosition = UpdatedPingPosition(displayedIRTargets[i].targetData.position, + displayedIRTargets[i].detectedByIRST); + newData.signalPersistTime = displayedIRTargets[i].signalPersistTime; + newData.targetData = displayedIRTargets[i].targetData; + newData.vessel = displayedIRTargets[i].vessel; + displayedIRTargets[i] = newData; + } + Vector2 pingPosition = displayedIRTargets[i].pingPosition; + + Rect pingRect; + + //float vAngle = Vector2.Angle(Vector3.up, pingPosition - Centerpoint); + float vAngle = Vector2.Angle(Vector3.up, Centerpoint - pingPosition); + if (pingPosition.x < Centerpoint.x) + { + vAngle = -vAngle; //FIXME - inverted. Need to Flip (not mirror) angle + } + + if ((displayedIRTargets[i].targetData.targetInfo && displayedIRTargets[i].targetData.targetInfo.isMissile) || displayedIRTargets[i].targetData.Team == null) + { + float mDotSize = (20) / (omniDisplay ? 1 : rangeIndex + 1); + if (mDotSize < 1) mDotSize = 1; + + if (omniDisplay) + { + GUIUtility.RotateAroundPivot(vAngle, Centerpoint); + pingRect = new Rect(Centerpoint.x - (mDotSize / 2), Centerpoint.y - (RadarDisplayRect.height / 2), mDotSize, mDotSize); + } + else pingRect = new Rect(pingPosition.x - (mDotSize / 2), pingPosition.y - (mDotSize / 2), mDotSize, mDotSize); + + Color origGUIColor = GUI.color; + GUI.color = Color.white - new Color(0, 0, 0, minusAlpha); + GUI.DrawTexture(pingRect, omniDisplay ? displayedIRTargets[i].detectedByIRST.irstRanging ? BDArmorySetup.Instance.redDotTexture : BDArmorySetup.Instance.irSpikeTexture : BDArmorySetup.Instance.redDotTexture, ScaleMode.StretchToFill, true); + GUI.color = origGUIColor; + + GUI.matrix = Matrix4x4.identity; + } + /* + else if (displayedIRTargets[i].detectedByIRST.showDirectionWhileScan && + displayedIRTargets[i].targetData.velocity.sqrMagnitude > 100) + { + pingRect = new Rect(pingPosition.x - (lockIconSize / 2), pingPosition.y - (lockIconSize / 2), + lockIconSize, lockIconSize); + float vAngle = + Vector3.Angle( + displayedIRTargets[i].targetData.velocity.ProjectOnPlanePreNormalized(referenceTransform.up), + referenceTransform.forward); + if (referenceTransform.InverseTransformVector(displayedIRTargets[i].targetData.velocity).x < 0) + { + vAngle = -vAngle; + } + GUIUtility.RotateAroundPivot(vAngle, pingPosition); + Color origGUIColor = GUI.color; + GUI.color = Color.white - new Color(0, 0, 0, minusAlpha); + if (weaponManager && + weaponManager.Team.IsFriendly(displayedIRTargets[i].targetData.Team)) + { + GUI.DrawTexture(pingRect, friendlyIRContactIcon, ScaleMode.StretchToFill, true); + } + else + { + GUI.DrawTexture(pingRect, irContactIcon, ScaleMode.StretchToFill, true); + } + + GUI.matrix = Matrix4x4.identity; + GUI.Label(new Rect(pingPosition.x + (lockIconSize * 0.35f) + 2, pingPosition.y, 100, 24), + (displayedIRTargets[i].targetData.altitude / 1000).ToString("0"), distanceStyle); + GUI.color = origGUIColor; + } + */ + //draw as dots + else + { + float mDotSize = (displayedIRTargets[i].magnitude / (omniDisplay ? 10 : 25)) / (omniDisplay ? 2 : rangeIndex + 1); + if (mDotSize < 1) mDotSize = 1; + if (mDotSize > (omniDisplay ? 80 : 20)) mDotSize = omniDisplay ? 80 : 20; + + if (omniDisplay) + { + GUIUtility.RotateAroundPivot(vAngle, Centerpoint); + pingRect = new Rect(Centerpoint.x - (mDotSize / 2), Centerpoint.y - (RadarDisplayRect.height / 2), mDotSize, mDotSize); + } + else pingRect = new Rect(pingPosition.x - (mDotSize / 2), pingPosition.y - (mDotSize / 2), mDotSize, mDotSize); + + Color origGUIColor = GUI.color; + GUI.color = Color.white - new Color(0, 0, 0, minusAlpha); + GUI.DrawTexture(pingRect, omniDisplay ? displayedIRTargets[i].detectedByIRST.irstRanging ? BDArmorySetup.Instance.redDotTexture : BDArmorySetup.Instance.irSpikeTexture : BDArmorySetup.Instance.redDotTexture, ScaleMode.StretchToFill, true); + GUI.color = origGUIColor; + + GUI.matrix = Matrix4x4.identity; + } + + if (BDArmorySettings.DEBUG_RADAR) + { + GUI.Label(new Rect(pingPosition.x + (pingSize.x / 2), pingPosition.y, 100, 24), + displayedIRTargets[i].magnitude.ToString("0.0")); + } + } + } } private bool omniDisplay { - get { return (radarCount > 1 || (radarCount == 1 && availableRadars[0].omnidirectional)); } + get { return (radarCount > 1 || (radarCount == 1 && availableRadars[0].omnidirectional) || irstCount > 1 || (irstCount == 1 && !availableIRSTs[0].irstRanging)); } } private void UpdateInputs() @@ -2034,32 +2413,50 @@ private void UpdateInputs() ShowSelector(); SlewSelector(Vector2.up); } - - if (BDInputUtils.GetKeyDown(BDInputSettingsFields.RADAR_LOCK)) + if (radarCount > 0) { - if (showSelector) + if (BDInputUtils.GetKeyDown(BDInputSettingsFields.RADAR_LOCK)) + { + if (showSelector) + { + TryLockViaSelector(); + } + ShowSelector(); + } + + if (BDInputUtils.GetKeyDown(BDInputSettingsFields.RADAR_CYCLE_LOCK)) { - TryLockViaSelector(); + if (locked) + { + CycleActiveLock(); + } } - ShowSelector(); - } - if (BDInputUtils.GetKeyDown(BDInputSettingsFields.RADAR_CYCLE_LOCK)) - { - if (locked) + if (BDInputUtils.GetKeyDown(BDInputSettingsFields.RADAR_TARGET_NEXT)) { - CycleActiveLock(); + TargetNext(); + } + else if (BDInputUtils.GetKeyDown(BDInputSettingsFields.RADAR_TARGET_PREV)) + { + TargetPrev(); } } - if (BDInputUtils.GetKeyDown(BDInputSettingsFields.RADAR_SCAN_MODE)) { - if (!locked && radarCount > 0 && !omniDisplay) + if (!locked && radarCount + irstCount > 0 && !omniDisplay) { availableRadars[0].boresightScan = !availableRadars[0].boresightScan; + availableIRSTs[0].boresightScan = !availableIRSTs[0].boresightScan; } } - + if (BDInputUtils.GetKeyDown(BDInputSettingsFields.RADAR_RANGE_UP)) + { + IncreaseRange(); + } + else if (BDInputUtils.GetKeyDown(BDInputSettingsFields.RADAR_RANGE_DN)) + { + DecreaseRange(); + } if (BDInputUtils.GetKeyDown(BDInputSettingsFields.RADAR_TURRETS)) { if (slaveTurrets) @@ -2071,24 +2468,6 @@ private void UpdateInputs() SlaveTurrets(); } } - - if (BDInputUtils.GetKeyDown(BDInputSettingsFields.RADAR_RANGE_UP)) - { - IncreaseRange(); - } - else if (BDInputUtils.GetKeyDown(BDInputSettingsFields.RADAR_RANGE_DN)) - { - DecreaseRange(); - } - - if (BDInputUtils.GetKeyDown(BDInputSettingsFields.RADAR_TARGET_NEXT)) - { - TargetNext(); - } - else if (BDInputUtils.GetKeyDown(BDInputSettingsFields.RADAR_TARGET_PREV)) - { - TargetPrev(); - } } private void TryLockViaSelector() @@ -2101,7 +2480,7 @@ private void TryLockViaSelector() float sqrMag = (displayedTargets[i].pingPosition - selectorPos).sqrMagnitude; if (sqrMag < closestSqrMag) { - if (sqrMag < Mathf.Pow(20, 2)) + if (sqrMag < 400) // 20 * 20) { closestPos = displayedTargets[i].targetData.predictedPosition; found = true; @@ -2113,7 +2492,7 @@ private void TryLockViaSelector() { TryLockTarget(closestPos); } - else if (closestSqrMag > Mathf.Pow(40, 2)) + else if (closestSqrMag > (40 * 40)) { UnlockCurrentTarget(); } diff --git a/BDArmory/Misc/ViewScanResults.cs b/BDArmory/Radar/ViewScanResults.cs similarity index 90% rename from BDArmory/Misc/ViewScanResults.cs rename to BDArmory/Radar/ViewScanResults.cs index cded8d269..f541ee300 100644 --- a/BDArmory/Misc/ViewScanResults.cs +++ b/BDArmory/Radar/ViewScanResults.cs @@ -1,8 +1,10 @@ using System.Collections.Generic; -using BDArmory.Modules; using UnityEngine; -namespace BDArmory.Misc +using BDArmory.Control; +using BDArmory.Weapons.Missiles; + +namespace BDArmory.Radar { public struct ViewScanResults { @@ -18,6 +20,7 @@ public struct ViewScanResults #region Guns public bool firingAtMe; public float missDistance; + public float missDeviation; public Vector3 threatPosition; public Vessel threatVessel; public MissileFire threatWeaponManager; diff --git a/BDArmory/Radar/_description b/BDArmory/Radar/_description index 9d713fbcc..b07719ebb 100644 --- a/BDArmory/Radar/_description +++ b/BDArmory/Radar/_description @@ -1 +1,2 @@ -Radar related modules and utils. \ No newline at end of file +Radar related modules and utils. +FIXME There's several functions in RadarUtils that aren't radar related. Also ViewScanResults. \ No newline at end of file diff --git a/BDArmory/Modules/IEngageService.cs b/BDArmory/Services/IEngageService.cs similarity index 89% rename from BDArmory/Modules/IEngageService.cs rename to BDArmory/Services/IEngageService.cs index e8936c0f8..46cdd4bb3 100644 --- a/BDArmory/Modules/IEngageService.cs +++ b/BDArmory/Services/IEngageService.cs @@ -1,4 +1,4 @@ -namespace BDArmory.Modules +namespace BDArmory.Services { public interface IEngageService { diff --git a/BDArmory.Core/Interface/INotificableService.cs b/BDArmory/Services/INotificableService.cs similarity index 83% rename from BDArmory.Core/Interface/INotificableService.cs rename to BDArmory/Services/INotificableService.cs index 4a2724b80..47c0185ed 100644 --- a/BDArmory.Core/Interface/INotificableService.cs +++ b/BDArmory/Services/INotificableService.cs @@ -1,6 +1,6 @@ using System; -namespace BDArmory.Core.Interface +namespace BDArmory.Services { public interface INotificableService where T : EventArgs { diff --git a/BDArmory.Core/Services/NotificableService.cs b/BDArmory/Services/NotificableService.cs similarity index 81% rename from BDArmory.Core/Services/NotificableService.cs rename to BDArmory/Services/NotificableService.cs index cdc24e0cb..71ebac1f1 100644 --- a/BDArmory.Core/Services/NotificableService.cs +++ b/BDArmory/Services/NotificableService.cs @@ -1,7 +1,6 @@ using System; -using BDArmory.Core.Interface; -namespace BDArmory.Core.Services +namespace BDArmory.Services { public abstract class NotificableService : INotificableService where T : EventArgs { diff --git a/BDArmory/Settings/BDAPersistentSettingsField.cs b/BDArmory/Settings/BDAPersistentSettingsField.cs new file mode 100644 index 000000000..00ccb11fd --- /dev/null +++ b/BDArmory/Settings/BDAPersistentSettingsField.cs @@ -0,0 +1,238 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using UniLinq; +using UnityEngine; + +namespace BDArmory.Settings +{ + [AttributeUsage(AttributeTargets.Field)] + public class BDAPersistentSettingsField : Attribute + { + public BDAPersistentSettingsField() + { + } + + /// + /// Save the current settings to the specified path. + /// + /// + public static void Save(string path) + { + ConfigNode fileNode = ConfigNode.Load(path); + if (fileNode == null) fileNode = new ConfigNode(); + fileNode.SetValue("VERSION", UI.BDArmorySetup.Version, true); + + if (!fileNode.HasNode("BDASettings")) + { + fileNode.AddNode("BDASettings"); + } + + ConfigNode settings = fileNode.GetNode("BDASettings"); + using (IEnumerator field = typeof(BDArmorySettings).GetFields().AsEnumerable().GetEnumerator()) + while (field.MoveNext()) + { + if (field.Current == null) continue; + if (!field.Current.IsDefined(typeof(BDAPersistentSettingsField), false)) continue; + + var fieldValue = field.Current.GetValue(null); + if (fieldValue.GetType() == typeof(Vector3d)) + { + settings.SetValue(field.Current.Name, ((Vector3d)fieldValue).ToString("G"), true); + } + else if (fieldValue.GetType() == typeof(Vector2d)) + { + settings.SetValue(field.Current.Name, ((Vector2d)fieldValue).ToString("G"), true); + } + else if (fieldValue.GetType() == typeof(List)) + { + settings.SetValue(field.Current.Name, string.Join("; ", (List)fieldValue), true); + } + else + { + settings.SetValue(field.Current.Name, fieldValue.ToString(), true); + } + } + fileNode.Save(path); + } + + /// + /// Load the settings from the default path. + /// + public static void Load() + { + ConfigNode fileNode = ConfigNode.Load(BDArmorySettings.settingsConfigURL); + if (!fileNode.HasNode("BDASettings")) return; + + ConfigNode settings = fileNode.GetNode("BDASettings"); + + using (IEnumerator field = typeof(BDArmorySettings).GetFields().AsEnumerable().GetEnumerator()) + while (field.MoveNext()) + { + if (field.Current == null) continue; + if (!field.Current.IsDefined(typeof(BDAPersistentSettingsField), false)) continue; + + if (!settings.HasValue(field.Current.Name)) continue; + object parsedValue = ParseValue(field.Current.FieldType, settings.GetValue(field.Current.Name)); + if (parsedValue != null) + { + field.Current.SetValue(null, parsedValue); + } + } + } + + /// + /// Check for settings that have been upgraded since the previously run version. + /// + public static void Upgrade() + { + ConfigNode fileNode = ConfigNode.Load(BDArmorySettings.settingsConfigURL); + ConfigNode oldDefaults = ConfigNode.Load(Path.ChangeExtension(BDArmorySettings.settingsConfigURL, ".default")); + + string version = "Unknown"; + if (fileNode.HasValue("VERSION")) + { + version = (string)fileNode.GetValue("VERSION"); + if (version == UI.BDArmorySetup.Version) return; // Already up to date. Do nothing. + } + Save(Path.ChangeExtension(BDArmorySettings.settingsConfigURL, ".default")); // Save the new defaults to settings.default + + if (!fileNode.HasNode("BDASettings")) return; // No settings, so they'll get generated on the first save. + + // Save the current settings to settings.old. + Debug.LogWarning($"[BDArmory.Settings]: BDArmory version differs from previous run: {UI.BDArmorySetup.Version} vs {version}. Saving previous config to {Path.ChangeExtension(BDArmorySettings.settingsConfigURL, ".old")} and upgrading settings."); + fileNode.Save(Path.ChangeExtension(BDArmorySettings.settingsConfigURL, ".old")); + + ConfigNode oldSettings = oldDefaults != null ? oldDefaults.GetNode("BDASettings") : null; + if (oldSettings == null) + { + Debug.LogWarning($"[BDArmory.Settings]: No previous default settings found, unable to check for default vs user changes."); + return; + } + + var excludedFields = new HashSet { "LAST_USED_SAVEGAME", }; // A bunch of other stuff is also excluded below. + ConfigNode settings = fileNode.GetNode("BDASettings"); + using (var field = typeof(BDArmorySettings).GetFields().AsEnumerable().GetEnumerator()) + while (field.MoveNext()) + { + if (field.Current == null) continue; + if (!field.Current.IsDefined(typeof(BDAPersistentSettingsField), false)) continue; + + bool skip = false; + if (excludedFields.Contains(field.Current.Name)) skip = true; // Skip excluded fields. + if (field.Current.Name.StartsWith("REMOTE_")) skip = true; // Skip remote API stuff. + if (field.Current.Name.StartsWith("EVOLUTION_")) skip = true; // Skip evolution stuff. + if (field.Current.Name.EndsWith("_WIDTH")) skip = true; // Skip window width stuff. + if (field.Current.Name.EndsWith("_OPTIONS")) skip = true; // Skip various section toggles. + if (field.Current.Name.EndsWith("_SETTINGS_TOGGLE")) skip = true; // Skip various section toggles. + + if (!settings.HasValue(field.Current.Name)) continue; + object currentValue = ParseValue(field.Current.FieldType, settings.GetValue(field.Current.Name)); + if (currentValue == null) continue; + var defaultValue = field.Current.GetValue(null); + if (!skip && currentValue is IComparable && ((IComparable)defaultValue).CompareTo((IComparable)currentValue) != 0) // The current value doesn't match the default. Note: Vector2d, Vector3d and List are not IComparable. + { + if (oldSettings.HasValue(field.Current.Name)) + { + object oldDefaultValue = ParseValue(field.Current.FieldType, oldSettings.GetValue(field.Current.Name)); + if (((IComparable)oldDefaultValue).CompareTo((IComparable)currentValue) == 0) // The current value matches the old default => upgrade it. + { + Debug.Log($"[BDArmory.Settings]: Upgrading {field.Current.Name} to the default {defaultValue}, from {currentValue}."); + field.Current.SetValue(null, defaultValue); + continue; + } + else Debug.Log($"[BDArmory.Settings]: {field.Current.Name} with value {currentValue} doesn't match either of the current or previous defaults, assuming it was modified by the user."); + } + else Debug.Log($"[BDArmory.Settings]: {field.Current.Name} with value {currentValue} doesn't match the current default and didn't exist in the previous defaults, assuming it was modified by the user."); + } + field.Current.SetValue(null, currentValue); // Use the current value. + } + Save(BDArmorySettings.settingsConfigURL); // Overwrite the settings with the modified ones. + } + + public static object ParseValue(Type type, string value) + { + try + { + if (type == typeof(string)) + { + return value; + } + + if (type == typeof(bool)) + { + return Boolean.Parse(value); + } + else if (type.IsEnum) + { + return System.Enum.Parse(type, value); + } + else if (type == typeof(float)) + { + return Single.Parse(value); + } + else if (type == typeof(int)) + { + return int.Parse(value); + } + else if (type == typeof(Single)) + { + return Single.Parse(value); + } + else if (type == typeof(Rect)) + { + string[] strings = value.Split(','); + int xVal = Int32.Parse(strings[0].Split(':')[1].Split('.')[0]); + int yVal = Int32.Parse(strings[1].Split(':')[1].Split('.')[0]); + int wVal = Int32.Parse(strings[2].Split(':')[1].Split('.')[0]); + int hVal = Int32.Parse(strings[3].Split(':')[1].Split('.')[0]); + Rect rectVal = new Rect + { + x = xVal, + y = yVal, + width = wVal, + height = hVal + }; + return rectVal; + } + else if (type == typeof(Vector2d)) + { + char[] charsToTrim = { '(', ')', ' ' }; + string[] strings = value.Trim(charsToTrim).Split(','); + double x = double.Parse(strings[0]); + double y = double.Parse(strings[1]); + return new Vector2d(x, y); + } + else if (type == typeof(Vector3d)) + { + char[] charsToTrim = { '[', ']', ' ' }; + string[] strings = value.Trim(charsToTrim).Split(','); + double x = double.Parse(strings[0]); + double y = double.Parse(strings[1]); + double z = double.Parse(strings[2]); + return new Vector3d(x, y, z); + } + else if (type == typeof(Vector2Int)) + { + char[] charsToTrim = { '(', ')', ' ' }; + string[] strings = value.Trim(charsToTrim).Split(','); + int x = int.Parse(strings[0]); + int y = int.Parse(strings[1]); + return new Vector2Int(x, y); + } + else if (type == typeof(List)) + { + return value.Split(new string[] { "; " }, StringSplitOptions.RemoveEmptyEntries).ToList(); + } + } + catch (Exception e) + { + Debug.LogError("[BDArmory.BDAPersistantSettingsField]: Failed to parse '" + value + "' as a " + type.ToString() + ": " + e.Message); + return null; + } + Debug.LogError("[BDArmory.BDAPersistantSettingsField]: BDAPersistantSettingsField to parse settings field of type " + type + " and value " + value); + return null; + } + } +} diff --git a/BDArmory/UI/BDAWindowSettingsField.cs b/BDArmory/Settings/BDAWindowSettingsField.cs similarity index 97% rename from BDArmory/UI/BDAWindowSettingsField.cs rename to BDArmory/Settings/BDAWindowSettingsField.cs index 2f6f7198e..6341f70fe 100644 --- a/BDArmory/UI/BDAWindowSettingsField.cs +++ b/BDArmory/Settings/BDAWindowSettingsField.cs @@ -1,10 +1,11 @@ using System; using System.Collections.Generic; using System.Reflection; -using BDArmory.Core; using UniLinq; -namespace BDArmory.UI +using BDArmory.UI; + +namespace BDArmory.Settings { [AttributeUsage(AttributeTargets.Field)] public class BDAWindowSettingsField : Attribute diff --git a/BDArmory.Core/BDArmorySettings.cs b/BDArmory/Settings/BDArmorySettings.cs similarity index 70% rename from BDArmory.Core/BDArmorySettings.cs rename to BDArmory/Settings/BDArmorySettings.cs index 81c3fd179..a22d64c7d 100644 --- a/BDArmory.Core/BDArmorySettings.cs +++ b/BDArmory/Settings/BDArmorySettings.cs @@ -3,26 +3,25 @@ using System.IO; using System.Collections.Generic; -namespace BDArmory.Core +namespace BDArmory.Settings { public class BDArmorySettings { public static string oldSettingsConfigURL = Path.Combine(KSPUtil.ApplicationRootPath, "GameData/BDArmory/settings.cfg"); // Migrate from the old settings file to the new one in PluginData so that we don't invalidate the ModuleManager cache. - public static string settingsConfigURL = Path.Combine(KSPUtil.ApplicationRootPath, "GameData/BDArmory/PluginData/settings.cfg"); + public static string settingsConfigURL = Path.GetFullPath(Path.Combine(KSPUtil.ApplicationRootPath, "GameData/BDArmory/PluginData/settings.cfg")); public static bool ready = false; // Settings section toggles [BDAPersistentSettingsField] public static bool GAMEPLAY_SETTINGS_TOGGLE = false; - [BDAPersistentSettingsField] public static bool GRAPHICS_UI_SECTION_TOGGLE = false; + [BDAPersistentSettingsField] public static bool GRAPHICS_UI_SETTINGS_TOGGLE = false; [BDAPersistentSettingsField] public static bool GAME_MODES_SETTINGS_TOGGLE = false; [BDAPersistentSettingsField] public static bool SLIDER_SETTINGS_TOGGLE = false; [BDAPersistentSettingsField] public static bool RADAR_SETTINGS_TOGGLE = false; [BDAPersistentSettingsField] public static bool OTHER_SETTINGS_TOGGLE = false; [BDAPersistentSettingsField] public static bool DEBUG_SETTINGS_TOGGLE = false; - [BDAPersistentSettingsField] public static bool COMPETITION_SETTINGS_TOGGLE = true; + [BDAPersistentSettingsField] public static bool COMPETITION_SETTINGS_TOGGLE = false; [BDAPersistentSettingsField] public static bool GM_SETTINGS_TOGGLE = false; - [BDAPersistentSettingsField] public static bool SPAWN_SETTINGS_TOGGLE = false; - [BDAPersistentSettingsField] public static bool ADVANDED_USER_SETTINGS = false; + [BDAPersistentSettingsField] public static bool ADVANCED_USER_SETTINGS = true; // Window settings [BDAPersistentSettingsField] public static bool STRICT_WINDOW_BOUNDARIES = true; @@ -33,33 +32,51 @@ public class BDArmorySettings [BDAPersistentSettingsField] public static bool VESSEL_SWITCHER_PERSIST_UI = false; [BDAPersistentSettingsField] public static float VESSEL_SPAWNER_WINDOW_WIDTH = 480f; [BDAPersistentSettingsField] public static float EVOLUTION_WINDOW_WIDTH = 350f; + [BDAPersistentSettingsField] public static float GUI_OPACITY = 1f; // Modify the GUI opacity. // General toggle settings - //[BDAPersistentSettingsField] public static bool INSTAKILL = true; //Depreciated, only affects lasers; use an Instagib mutator isntead + //[BDAPersistentSettingsField] public static bool INSTAKILL = true; //Deprecated, only affects lasers; use an Instagib mutator isntead [BDAPersistentSettingsField] public static bool AI_TOOLBAR_BUTTON = true; // Show or hide the BDA AI toolbar button. - [BDAPersistentSettingsField] public static bool INFINITE_AMMO = false; + [BDAPersistentSettingsField] public static bool VM_TOOLBAR_BUTTON = true; // Show or hide the BDA VM toolbar button. + [BDAPersistentSettingsField] public static bool INFINITE_AMMO = false; //infinite Bullets/rockets/laserpower + [BDAPersistentSettingsField] public static bool INFINITE_ORDINANCE = false; //infinite missiles/bombs (on ordinance w/ Reload Module) + [BDAPersistentSettingsField] public static bool INFINITE_FUEL = false; //Infinite propellant + [BDAPersistentSettingsField] public static bool INFINITE_EC = false; //Infinite electric charge [BDAPersistentSettingsField] public static bool BULLET_HITS = true; [BDAPersistentSettingsField] public static bool EJECT_SHELLS = true; [BDAPersistentSettingsField] public static bool VESSEL_RELATIVE_BULLET_CHECKS = false; [BDAPersistentSettingsField] public static bool AIM_ASSIST = true; + [BDAPersistentSettingsField] public static bool AIM_ASSIST_MODE = true; // true = reticle follows bullet CPA position, false = reticle follows aiming position. [BDAPersistentSettingsField] public static bool DRAW_AIMERS = true; + [BDAPersistentSettingsField] public static bool RESTORE_KAL = true; // Restore the Part, Module and AxisField references on the KAL to make it work. + // Debug Labels - [BDAPersistentSettingsField] public static bool DRAW_DEBUG_LINES = false; //AI/Weapon aim visualizers - [BDAPersistentSettingsField] public static bool DRAW_DEBUG_LABELS = false; - [BDAPersistentSettingsField] public static bool DRAW_ARMOR_LABELS = false; //armor only debug messages, for testing/debugging. remove/revert back to debug_labels later + [BDAPersistentSettingsField] public static bool DEBUG_LINES = false; //AI/Weapon aim visualizers + [BDAPersistentSettingsField] public static bool DEBUG_OTHER = false; //internal debugging + [BDAPersistentSettingsField] public static bool DEBUG_ARMOR = false; //armor and HP + [BDAPersistentSettingsField] public static bool DEBUG_WEAPONS = false; //Debug messages for guns/rockets/lasers and their projectiles + [BDAPersistentSettingsField] public static bool DEBUG_MISSILES = false; //Missile launch, tracking and targeting debug labels + [BDAPersistentSettingsField] public static bool DEBUG_DAMAGE = false; //Explosions and battle damage logging + [BDAPersistentSettingsField] public static bool DEBUG_AI = false; //AI debugging + [BDAPersistentSettingsField] public static bool DEBUG_RADAR = false; //FLIR/Radar and RCS debugging + [BDAPersistentSettingsField] public static bool DEBUG_TELEMETRY = false; //AI/WM UI debug telemetry display + [BDAPersistentSettingsField] public static bool DEBUG_SPAWNING = false; //Spawning debugging + [BDAPersistentSettingsField] public static bool DEBUG_COMPETITION = false; //Competition debugging [BDAPersistentSettingsField] public static bool REMOTE_SHOOTING = false; [BDAPersistentSettingsField] public static bool BOMB_CLEARANCE_CHECK = false; - [BDAPersistentSettingsField] public static bool SHOW_AMMO_GAUGES = false; + [BDAPersistentSettingsField] public static bool SHOW_AMMO_GAUGES = true; [BDAPersistentSettingsField] public static bool SHELL_COLLISIONS = true; [BDAPersistentSettingsField] public static bool BULLET_DECALS = true; - [BDAPersistentSettingsField] public static bool DISABLE_RAMMING = true; // Prevent craft from going into ramming mode when out of ammo. - [BDAPersistentSettingsField] public static bool DEFAULT_FFA_TARGETING = false; // Free-for-all combat style instead of teams (changes target selection behaviour) + [BDAPersistentSettingsField] public static bool GAPLESS_PARTICLE_EMITTERS = true; // Use gapless particle emitters. + [BDAPersistentSettingsField] public static bool FLARE_SMOKE = true; // Flares leave a trail of smoke. + [BDAPersistentSettingsField] public static bool DISABLE_RAMMING = false; // Prevent craft from going into ramming mode when out of ammo. + [BDAPersistentSettingsField] public static bool DEFAULT_FFA_TARGETING = false; // Free-for-all combat style instead of teams (changes target selection behaviour). This could be removed now. [BDAPersistentSettingsField] public static bool RUNWAY_PROJECT = false; // Enable/disable Runway Project specific enhancements. //[BDAPersistentSettingsField] public static bool DISABLE_KILL_TIMER = true; //disables the kill timers. [BDAPersistentSettingsField] public static bool AUTO_ENABLE_VESSEL_SWITCHING = false; // Automatically enables vessel switching on competition start. [BDAPersistentSettingsField] public static bool AUTONOMOUS_COMBAT_SEATS = false; // Enable/disable seats without kerbals. - [BDAPersistentSettingsField] public static bool DESTROY_UNCONTROLLED_WMS = false; // Automatically destroy the WM if there's no kerbal or drone core controlling it. + [BDAPersistentSettingsField] public static bool DESTROY_UNCONTROLLED_WMS = true; // Automatically destroy the WM if there's no kerbal or drone core controlling it. [BDAPersistentSettingsField] public static bool RESET_HP = false; // Automatically reset HP of parts of vessels when they're spawned in flight mode. [BDAPersistentSettingsField] public static bool RESET_ARMOUR = false; // Automatically reset Armor material of parts of vessels when they're spawned in flight mode. [BDAPersistentSettingsField] public static bool RESET_HULL = false; // Automatically reset hull material of parts of vessels when they're spawned in flight mode. @@ -67,7 +84,6 @@ public class BDArmorySettings [BDAPersistentSettingsField] public static bool TRACE_VESSELS_DURING_COMPETITIONS = false; // Trace vessel positions and rotations during competitions. [BDAPersistentSettingsField] public static bool DRAW_VESSEL_TRAILS = true; // Draw a trail to visualize vessel path during the heat [BDAPersistentSettingsField] public static int VESSEL_TRAIL_LENGTH = 300; //Max length of trails, in seconds. Defaults to competition length - [BDAPersistentSettingsField] public static bool DUMB_IR_SEEKERS = false; // IR missiles will go after hottest thing they can see [BDAPersistentSettingsField] public static bool AUTOCATEGORIZE_PARTS = true; [BDAPersistentSettingsField] public static bool SHOW_CATEGORIES = true; [BDAPersistentSettingsField] public static bool IGNORE_TERRAIN_CHECK = false; @@ -75,6 +91,7 @@ public class BDArmorySettings //[BDAPersistentSettingsField] public static bool ADVANCED_EDIT = true; //Used for debug fields not nomrally shown to regular users //SI - Only usage is a commented out function in BDExplosivePart [BDAPersistentSettingsField] public static bool DISPLAY_COMPETITION_STATUS = true; //Display competition status [BDAPersistentSettingsField] public static bool DISPLAY_COMPETITION_STATUS_WITH_HIDDEN_UI = false; // Display the competition status when using the "hidden UI" + [BDAPersistentSettingsField] public static bool SCROLL_ZOOM_PREVENTION = true; // Prevent scroll-zoom when over most BDA windows. [BDAPersistentSettingsField] public static bool BULLET_WATER_DRAG = true; [BDAPersistentSettingsField] public static bool PERSISTENT_FX = false; [BDAPersistentSettingsField] public static bool LEGACY_ARMOR = false; @@ -84,38 +101,49 @@ public class BDArmorySettings [BDAPersistentSettingsField] public static bool GENERATE_CLEAN_SAVE = false; // Use a clean save instead of the persistent one when loading to the KSC. // General slider settings - [BDAPersistentSettingsField] public static int COMPETITION_DURATION = 5; // Competition duration in minutes - [BDAPersistentSettingsField] public static float COMPETITION_INITIAL_GRACE_PERIOD = 60; // Competition initial grace period in seconds. + [BDAPersistentSettingsField] public static int COMPETITION_DURATION = 0; // Competition duration in minutes (0=unlimited) + [BDAPersistentSettingsField] public static float COMPETITION_INITIAL_GRACE_PERIOD = 10; // Competition initial grace period in seconds. [BDAPersistentSettingsField] public static float COMPETITION_FINAL_GRACE_PERIOD = 10; // Competition final grace period in seconds. [BDAPersistentSettingsField] public static float COMPETITION_KILL_TIMER = 15; // Competition kill timer in seconds. [BDAPersistentSettingsField] public static float COMPETITION_KILLER_GM_FREQUENCY = 60; // Competition killer GM timer in seconds. [BDAPersistentSettingsField] public static float COMPETITION_KILLER_GM_GRACE_PERIOD = 150; // Competition killer GM grace period in seconds. - [BDAPersistentSettingsField] public static float COMPETITION_ALTITUDE_LIMIT_HIGH = 46; // Altitude (high) in km at which to kill off craft. - [BDAPersistentSettingsField] public static float COMPETITION_ALTITUDE_LIMIT_LOW = -1; // Altitude (low) in km at which to kill off craft. + [BDAPersistentSettingsField] public static float COMPETITION_ALTITUDE_LIMIT_HIGH = 55; // Altitude (high) in km at which to kill off craft. + [BDAPersistentSettingsField] public static float COMPETITION_ALTITUDE_LIMIT_LOW = -39; // Altitude (low) in km at which to kill off craft. + [BDAPersistentSettingsField] public static bool COMPETITION_ALTITUDE__LIMIT_ASL = false; // Does Killer GM use ASL or AGL for latitide ceiling/floor? [BDAPersistentSettingsField] public static float COMPETITION_NONCOMPETITOR_REMOVAL_DELAY = 30; // Competition non-competitor removal delay in seconds. + [BDAPersistentSettingsField] public static float COMPETITION_WAYPOINTS_GM_KILL_PERIOD = 60; // Waypoint Competition GM kill period in seconds. Craft that don't pass a waypoint within this time are killed off. [BDAPersistentSettingsField] public static float COMPETITION_DISTANCE = 1000; // Competition distance. + [BDAPersistentSettingsField] public static float COMPETITION_INTRA_TEAM_SEPARATION_BASE = 800; // Intra-team separation (base value). + [BDAPersistentSettingsField] public static float COMPETITION_INTRA_TEAM_SEPARATION_PER_MEMBER = 100; // Intra-team separation (per member value). [BDAPersistentSettingsField] public static int COMPETITION_START_NOW_AFTER = 11; // Competition auto-start now. + [BDAPersistentSettingsField] public static bool COMPETITION_START_DESPITE_FAILURES = false; // Start competition despite failures. [BDAPersistentSettingsField] public static float DEBRIS_CLEANUP_DELAY = 15f; // Clean up debris after 30s. [BDAPersistentSettingsField] public static int MAX_NUM_BULLET_DECALS = 200; [BDAPersistentSettingsField] public static int TERRAIN_ALERT_FREQUENCY = 1; // Controls how often terrain avoidance checks are made (gets scaled by 1+(radarAltitude/500)^2) - [BDAPersistentSettingsField] public static int CAMERA_SWITCH_FREQUENCY = 3; // Controls the minimum time between automated camera switches - [BDAPersistentSettingsField] public static int DEATH_CAMERA_SWITCH_INHIBIT_PERIOD = 0; // Controls the delay before the next switch after the currently active vessel dies + [BDAPersistentSettingsField] public static int CAMERA_SWITCH_FREQUENCY = 10; // Controls the minimum time between automated camera switches + [BDAPersistentSettingsField] public static int DEATH_CAMERA_SWITCH_INHIBIT_PERIOD = 2; // Controls the delay before the next switch after the currently active vessel dies + [BDAPersistentSettingsField] public static bool CAMERA_SWITCH_INCLUDE_MISSILES = false; // Include missiles in the camera switching logic. [BDAPersistentSettingsField] public static int KERBAL_SAFETY_INVENTORY = 2; // Controls how Kerbal Safety adjusts the inventory of kerbals. [BDAPersistentSettingsField] public static float TRIGGER_HOLD_TIME = 0.2f; [BDAPersistentSettingsField] public static float BDARMORY_UI_VOLUME = 0.35f; [BDAPersistentSettingsField] public static float BDARMORY_WEAPONS_VOLUME = 0.45f; [BDAPersistentSettingsField] public static float MAX_GUARD_VISUAL_RANGE = 200000f; [BDAPersistentSettingsField] public static float MAX_ACTIVE_RADAR_RANGE = 200000f; //NOTE: used ONLY for display range of radar windows! Actual radar range provided by part configs! + [BDAPersistentSettingsField] public static bool LOGARITHMIC_RADAR_DISPLAY = true; //NOTE: used ONLY for display range of radar windows! Actual radar range provided by part configs! [BDAPersistentSettingsField] public static float MAX_ENGAGEMENT_RANGE = 200000f; //NOTE: used ONLY for missile dlz parameters! [BDAPersistentSettingsField] public static float IVA_LOWPASS_FREQ = 2500f; - [BDAPersistentSettingsField] public static float SMOKE_DEFLECTION_FACTOR = 10f; [BDAPersistentSettingsField] public static float BALLISTIC_TRAJECTORY_SIMULATION_MULTIPLIER = 256f; // Multiplier of fixedDeltaTime for the large scale steps of ballistic trajectory simulations. [BDAPersistentSettingsField] public static float FIRE_RATE_OVERRIDE = 10f; [BDAPersistentSettingsField] public static float FIRE_RATE_OVERRIDE_CENTER = 20f; [BDAPersistentSettingsField] public static float FIRE_RATE_OVERRIDE_SPREAD = 5f; - [BDAPersistentSettingsField] public static float FIRE_RATE_OVERRIDE_BIAS = 0.16f; + [BDAPersistentSettingsField] public static float FIRE_RATE_OVERRIDE_BIAS = 0.4f; [BDAPersistentSettingsField] public static float FIRE_RATE_OVERRIDE_HIT_MULTIPLIER = 2f; - + [BDAPersistentSettingsField] public static float HP_THRESHOLD = 2000; //HP above this value will be scaled to a logarithmic value + [BDAPersistentSettingsField] public static float HP_CLAMP = 0; //HP will be clamped to this value + [BDAPersistentSettingsField] public static bool PWING_EDGE_LIFT = true; //Toggle lift on PWing edges for balance with stock wings/remove edge abuse + [BDAPersistentSettingsField] public static bool PWING_THICKNESS_AFFECT_MASS_HP = false; //pWing thickness contributes to its mass alc instead of a static LiftArea derived value + [BDAPersistentSettingsField] public static float MAX_PWING_LIFT = 4.54f; //Clamp pWing lift to this amount + [BDAPersistentSettingsField] public static float MAX_SAS_TORQUE = 30; //Clamp vessel total non-cockpit torque to this // Physics constants [BDAPersistentSettingsField] public static float GLOBAL_LIFT_MULTIPLIER = 0.25f; [BDAPersistentSettingsField] public static float GLOBAL_DRAG_MULTIPLIER = 6f; @@ -123,15 +151,17 @@ public class BDArmorySettings [BDAPersistentSettingsField] public static float DMG_MULTIPLIER = 100f; [BDAPersistentSettingsField] public static float BALLISTIC_DMG_FACTOR = 1.55f; [BDAPersistentSettingsField] public static float HITPOINT_MULTIPLIER = 3.0f; - [BDAPersistentSettingsField] public static float EXP_DMG_MOD_BALLISTIC_NEW = 0.65f; // HE bullet explosion damage multiplier + [BDAPersistentSettingsField] public static float EXP_DMG_MOD_BALLISTIC_NEW = 0.55f; // HE bullet explosion damage multiplier + [BDAPersistentSettingsField] public static float EXP_PEN_RESIST_MULT = 2.50f; // Armor HE penetration resistance multiplier [BDAPersistentSettingsField] public static float EXP_DMG_MOD_MISSILE = 6.75f; // Missile explosion damage multiplier [BDAPersistentSettingsField] public static float EXP_DMG_MOD_ROCKET = 1f; // Rocket explosion damage multiplier (FIXME needs tuning; Note: rockets used Ballistic mod before, but probably ought to be more like missiles) [BDAPersistentSettingsField] public static float EXP_DMG_MOD_BATTLE_DAMAGE = 1f; // Battle damage explosion damage multiplier (FIXME needs tuning; Note: CASE-0 explosions used Missile mod, while CASE-1, CASE-2 and fuel explosions used Ballistic mod) [BDAPersistentSettingsField] public static float EXP_IMP_MOD = 0.25f; + [BDAPersistentSettingsField] public static float BUILDING_DMG_MULTIPLIER = 1f; [BDAPersistentSettingsField] public static bool EXTRA_DAMAGE_SLIDERS = false; [BDAPersistentSettingsField] public static float WEAPON_FX_DURATION = 15; //how long do weapon secondary effects(EMP/choker/gravitic/etc) last [BDAPersistentSettingsField] public static float ZOMBIE_DMG_MULT = 0.1f; - + [BDAPersistentSettingsField] public static float ARMOR_MASS_MOD = 1f; //Armor mass multiplier // FX [BDAPersistentSettingsField] public static bool FIRE_FX_IN_FLIGHT = false; [BDAPersistentSettingsField] public static int MAX_FIRES_PER_VESSEL = 10; //controls fx for penetration only for landed or splashed //this is only for physical missile collisons into fueltanks - SI @@ -157,7 +187,8 @@ public class BDArmorySettings [BDAPersistentSettingsField] public static bool TAG_MODE = false; [BDAPersistentSettingsField] public static bool PAINTBALL_MODE = false; [BDAPersistentSettingsField] public static bool GRAVITY_HACKS = false; - [BDAPersistentSettingsField] public static bool BATTLEDAMAGE = false; + [BDAPersistentSettingsField] public static bool ALTITUDE_HACKS = false; //transfer to a RunWayRound number? + [BDAPersistentSettingsField] public static bool BATTLEDAMAGE = true; [BDAPersistentSettingsField] public static bool HEART_BLEED_ENABLED = false; [BDAPersistentSettingsField] public static bool RESOURCE_STEAL_ENABLED = false; [BDAPersistentSettingsField] public static bool ASTEROID_FIELD = false; @@ -178,6 +209,7 @@ public class BDArmorySettings [BDAPersistentSettingsField] public static bool DISCO_MODE = false; [BDAPersistentSettingsField] public static bool NO_ENGINES = false; [BDAPersistentSettingsField] public static bool WAYPOINTS_MODE = false; // Waypoint section of Vessel Spawner Window. + [BDAPersistentSettingsField] public static string PINATA_NAME = "Pinata"; //Battle Damage settings [BDAPersistentSettingsField] public static bool BATTLEDAMAGE_TOGGLE = false; // Main battle damage toggle. @@ -191,7 +223,7 @@ public class BDArmorySettings [BDAPersistentSettingsField] public static bool BD_PROPULSION = true; // Engine thrust reduction, fires [BDAPersistentSettingsField] public static float BD_PROP_FLOOR = 20; // Minimum thrust% damaged engines produce [BDAPersistentSettingsField] public static float BD_PROP_FLAMEOUT = 25; // Remaining HP% engines flameout - [BDAPersistentSettingsField] public static bool BD_BALANCED_THRUST = true; + [BDAPersistentSettingsField] public static bool BD_PART_STRENGTH = false; // Part strength - breakingForce/Torque - decreases as part takes damage [BDAPersistentSettingsField] public static float BD_PROP_DAM_RATE = 1; // Rate multiplier, 0.1-2 [BDAPersistentSettingsField] public static bool BD_INTAKES = true; // Can intakes be damaged? [BDAPersistentSettingsField] public static bool BD_GIMBALS = true; // Can gimbals be disabled? @@ -209,13 +241,14 @@ public class BDArmorySettings [BDAPersistentSettingsField] public static float BD_FIRE_CHANCE_TRACER = 10; [BDAPersistentSettingsField] public static float BD_FIRE_CHANCE_HE = 25; [BDAPersistentSettingsField] public static float BD_FIRE_CHANCE_INCENDIARY = 90; - [BDAPersistentSettingsField] public static bool ALLOW_ZOMBIE_BD = false; // Allow battle damage to proc when using zombie mode? + [BDAPersistentSettingsField] public static bool ALLOW_ZOMBIE_BD = false; // Allow battle damage to proc when using zombie mode? [BDAPersistentSettingsField] public static bool ENABLE_HOS = false; [BDAPersistentSettingsField] public static List HALL_OF_SHAME_LIST = new List(); [BDAPersistentSettingsField] public static float HOS_FIRE = 0; [BDAPersistentSettingsField] public static float HOS_MASS = 0; [BDAPersistentSettingsField] public static float HOS_DMG = 0; [BDAPersistentSettingsField] public static float HOS_THRUST = 0; + [BDAPersistentSettingsField] public static bool HOS_SAS = false; [BDAPersistentSettingsField] public static string HOS_BADGE = ""; // Remote logging @@ -227,34 +260,54 @@ public class BDArmorySettings [BDAPersistentSettingsField] public static float REMOTE_INTERHEAT_DELAY = 30; // Delay between heats. [BDAPersistentSettingsField] public static int RUNWAY_PROJECT_ROUND = 10; // RWP round index. [BDAPersistentSettingsField] public static string REMOTE_ORCHESTRATION_NPC_SWAPPER = "Rammer"; + [BDAPersistentSettingsField] public static string REMOTE_ORC_NPCS_TEAM = ""; // Spawner settings [BDAPersistentSettingsField] public static bool SHOW_SPAWN_OPTIONS = true; // Show spawn options. [BDAPersistentSettingsField] public static Vector2d VESSEL_SPAWN_GEOCOORDS = new Vector2d(0.05096, -74.8016); // Spawning coordinates on a planetary body; Lat, Lon [BDAPersistentSettingsField] public static int VESSEL_SPAWN_WORLDINDEX = 1; // Spawning planetary body: world index [BDAPersistentSettingsField] public static float VESSEL_SPAWN_ALTITUDE = 5f; // Spawning altitude above the surface. + public static float VESSEL_SPAWN_ALTITUDE_ => !RUNWAY_PROJECT ? VESSEL_SPAWN_ALTITUDE : RUNWAY_PROJECT_ROUND == 33 ? 10 : RUNWAY_PROJECT_ROUND == 53 ? FlightGlobals.currentMainBody.atmosphere ? (float)(FlightGlobals.currentMainBody.atmosphereDepth + (FlightGlobals.currentMainBody.atmosphereDepth / 10)) : 50000 : VESSEL_SPAWN_ALTITUDE; // Getter for handling the various RWP cases. [BDAPersistentSettingsField] public static float VESSEL_SPAWN_DISTANCE_FACTOR = 20f; // Scale factor for the size of the spawning circle. - [BDAPersistentSettingsField] public static float VESSEL_SPAWN_DISTANCE = 10f; // Radius of the size of the spawning circle. - [BDAPersistentSettingsField] public static bool VESSEL_SPAWN_DISTANCE_TOGGLE = false; // Toggle between scaling factor and absolute distance. + [BDAPersistentSettingsField] public static float VESSEL_SPAWN_DISTANCE = 100f; // Radius of the size of the spawning circle. + [BDAPersistentSettingsField] public static bool VESSEL_SPAWN_DISTANCE_TOGGLE = true; // Toggle between scaling factor and absolute distance. [BDAPersistentSettingsField] public static bool VESSEL_SPAWN_REASSIGN_TEAMS = true; // Reassign teams on spawn, overriding teams defined in the SPH. - [BDAPersistentSettingsField] public static float VESSEL_SPAWN_EASE_IN_SPEED = 1f; // Rate to limit "falling" during spawning. [BDAPersistentSettingsField] public static int VESSEL_SPAWN_CONCURRENT_VESSELS = 0; // Maximum number of vessels to spawn in concurrently (continuous spawning mode). [BDAPersistentSettingsField] public static int VESSEL_SPAWN_LIVES_PER_VESSEL = 0; // Maximum number of times to spawn a vessel (continuous spawning mode). [BDAPersistentSettingsField] public static float OUT_OF_AMMO_KILL_TIME = -1f; // Out of ammo kill timer for continuous spawn mode. - [BDAPersistentSettingsField] public static int VESSEL_SPAWN_FILL_SEATS = 1; // Fill seats: 0 - minimal, 1 - all ModuleCommand and KerbalSeat parts, 2 - also cabins. + [BDAPersistentSettingsField] public static int VESSEL_SPAWN_FILL_SEATS = 1; // Fill seats: 0 - minimal, 1 - full cockpits or the first combat seat, 2 - all ModuleCommand and KerbalSeat parts, 3 - also cabins. [BDAPersistentSettingsField] public static bool VESSEL_SPAWN_CONTINUE_SINGLE_SPAWNING = false; // Spawn craft again after single spawn competition finishes. [BDAPersistentSettingsField] public static bool VESSEL_SPAWN_DUMP_LOG_EVERY_SPAWN = false; // Dump competition scores every time a vessel spawns. [BDAPersistentSettingsField] public static bool SHOW_SPAWN_LOCATIONS = false; // Show the interesting spawn locations. [BDAPersistentSettingsField] public static int VESSEL_SPAWN_NUMBER_OF_TEAMS = 0; // Number of Teams: 0 - FFA, 1 - Folders, 2-10 specified directly [BDAPersistentSettingsField] public static string VESSEL_SPAWN_FILES_LOCATION = ""; // Spawn files location (under AutoSpawn). + [BDAPersistentSettingsField] public static string VESSEL_SPAWN_GAUNTLET_OPPONENTS_FILES_LOCATION = ""; // Gauntlet opponents spawn files location (under AutoSpawn). [BDAPersistentSettingsField] public static bool VESSEL_SPAWN_RANDOM_ORDER = true; // Shuffle vessels before spawning them. - [BDAPersistentSettingsField] public static bool SHOW_WAYPOINTS_OPTIONS = true; // Waypoint section of Vessel Spawner Window. + [BDAPersistentSettingsField] public static bool SHOW_WAYPOINTS_OPTIONS = true; // Waypoint section of Vessel Spawner Window. + [BDAPersistentSettingsField] public static bool VESSEL_SPAWN_START_COMPETITION_AUTOMATICALLY = false; // Automatically start a competition after spawning succeeds. + + // Vessel Mover settings + [BDAPersistentSettingsField] public static bool VESSEL_MOVER_CHOOSE_CREW = false; // Choose crew when spawning vessels. + [BDAPersistentSettingsField] public static bool VESSEL_MOVER_CLASSIC_CRAFT_CHOOSER = false; // Use the built-in craft chooser instead of the custom one. + [BDAPersistentSettingsField] public static bool VESSEL_MOVER_ENABLE_BRAKES = true; // Enable brakes when spawning vessels. + [BDAPersistentSettingsField] public static bool VESSEL_MOVER_ENABLE_SAS = true; // Enable SAS when spawning vessels. + [BDAPersistentSettingsField] public static float VESSEL_MOVER_MIN_LOWER_SPEED = 1f; // Minimum speed to lower vessels. + [BDAPersistentSettingsField] public static bool VESSEL_MOVER_LOWER_FAST = true; // Skip lowering from high altitude. + [BDAPersistentSettingsField] public static bool VESSEL_MOVER_BELOW_WATER = false; // Lower below water (on planets that have water). + [BDAPersistentSettingsField] public static bool VESSEL_MOVER_DONT_WORRY_ABOUT_COLLISIONS = false; // Don't prevent collisions. + [BDAPersistentSettingsField] public static bool VESSEL_MOVER_CLOSE_ON_COMPETITION_START = true; // Close when starting a competition. + [BDAPersistentSettingsField] public static bool VESSEL_MOVER_PLACE_AFTER_SPAWN = false; // Immediately place vessels after spawning them. // Waypoints - [BDAPersistentSettingsField] public static float WAYPOINTS_ALTITUDE = 50f; // Altitude above ground of the waypoints. - [BDAPersistentSettingsField] public static bool WAYPOINTS_ONE_AT_A_TIME = true; // Send the craft one-at-a-time through the course. - [BDAPersistentSettingsField] public static bool WAYPOINTS_VISUALIZE = true; // Add Waypoint models to indicate the path - [BDAPersistentSettingsField] public static float WAYPOINTS_SCALE = 500; // Have model(or maybe WP radius proper) scale? + [BDAPersistentSettingsField] public static float WAYPOINTS_ALTITUDE = 0f; // Altitude above ground of the waypoints. + [BDAPersistentSettingsField] public static bool WAYPOINTS_ONE_AT_A_TIME = false; // Send the craft one-at-a-time through the course. + [BDAPersistentSettingsField] public static bool WAYPOINTS_VISUALIZE = true; // Add Waypoint models to indicate the path + [BDAPersistentSettingsField] public static bool WAYPOINTS_INFINITE_FUEL_AT_START = true; // Don't consume fuel prior to the first waypoint. + [BDAPersistentSettingsField] public static float WAYPOINTS_SCALE = 0f; // Have model(or maybe WP radius proper) scale? + [BDAPersistentSettingsField] public static int WAYPOINT_COURSE_INDEX = 0; // Select from a set of courses + [BDAPersistentSettingsField] public static int WAYPOINT_LOOP_INDEX = 1; // Number of loops to generate + [BDAPersistentSettingsField] public static int WAYPOINT_GUARD_INDEX = -1; // Activate guard after index; -1 for no guard + // Heartbleed [BDAPersistentSettingsField] public static float HEART_BLEED_RATE = 0.01f; [BDAPersistentSettingsField] public static float HEART_BLEED_INTERVAL = 10f; @@ -272,6 +325,7 @@ public class BDArmorySettings [BDAPersistentSettingsField] public static bool SF_FRICTION = false; [BDAPersistentSettingsField] public static bool SF_GRAVITY = false; [BDAPersistentSettingsField] public static bool SF_REPULSOR = false; + [BDAPersistentSettingsField] public static float SF_REPULSOR_STRENGTH = 5f; [BDAPersistentSettingsField] public static float SF_DRAGMULT = 2f; //Mutator Mode @@ -286,13 +340,15 @@ public class BDArmorySettings // Tournament settings [BDAPersistentSettingsField] public static bool SHOW_TOURNAMENT_OPTIONS = false; // Show tournament options. [BDAPersistentSettingsField] public static int TOURNAMENT_STYLE = 0; // Tournament Style (Random, N-choose-K, etc.) - [BDAPersistentSettingsField] public static float TOURNAMENT_DELAY_BETWEEN_HEATS = 10; // Delay between heats + [BDAPersistentSettingsField] public static float TOURNAMENT_DELAY_BETWEEN_HEATS = 5; // Delay between heats [BDAPersistentSettingsField] public static int TOURNAMENT_ROUNDS = 1; // Rounds [BDAPersistentSettingsField] public static int TOURNAMENT_ROUNDS_CUSTOM = 1000; // Custom number of rounds at right end of slider. - [BDAPersistentSettingsField] public static int TOURNAMENT_VESSELS_PER_HEAT = 8; // Vessels Per Heat - [BDAPersistentSettingsField] public static Vector2Int TOURNAMENT_AUTO_VESSELS_PER_HEAT_RANGE = new Vector2Int(6, 8); // Automatic vessels per heat selection (inclusive range). + [BDAPersistentSettingsField] public static int TOURNAMENT_VESSELS_PER_HEAT = -1; // Vessels Per Heat (Auto) + [BDAPersistentSettingsField] public static Vector2Int TOURNAMENT_AUTO_VESSELS_PER_HEAT_RANGE = new Vector2Int(6, 10); // Automatic vessels per heat selection (inclusive range). [BDAPersistentSettingsField] public static int TOURNAMENT_TEAMS_PER_HEAT = 2; // Teams Per Heat + [BDAPersistentSettingsField] public static int TOURNAMENT_OPPONENT_TEAMS_PER_HEAT = 1; // Opponent Teams Per Heat (for gauntlets) [BDAPersistentSettingsField] public static int TOURNAMENT_VESSELS_PER_TEAM = 2; // Vessels Per Team + [BDAPersistentSettingsField] public static int TOURNAMENT_OPPONENT_VESSELS_PER_TEAM = 2; // Opponent Vessels Per Team [BDAPersistentSettingsField] public static bool TOURNAMENT_FULL_TEAMS = true; // Full Teams [BDAPersistentSettingsField] public static float TOURNAMENT_TIMEWARP_BETWEEN_ROUNDS = 0; // Timewarp between rounds in minutes. [BDAPersistentSettingsField] public static bool AUTO_RESUME_TOURNAMENT = false; // Automatically load the game the last incomplete tournament was running in and continue the tournament. @@ -300,10 +356,16 @@ public class BDArmorySettings [BDAPersistentSettingsField] public static bool AUTO_QUIT_AT_END_OF_TOURNAMENT = false; // Automatically quit at the end of a tournament (for automation). [BDAPersistentSettingsField] public static bool AUTO_GENERATE_TOURNAMENT_ON_RESUME = false; // Automatically generate a tournament after loading the game if the last tournament was complete or missing. [BDAPersistentSettingsField] public static string LAST_USED_SAVEGAME = ""; // Name of the last used savegame (for auto_generate_tournament_on_resume). + [BDAPersistentSettingsField] public static bool AUTO_DISABLE_UI = false; // Automatically disable the UI when starting tournaments. + + // Custom Spawn Template + [BDAPersistentSettingsField] public static bool CUSTOM_SPAWN_TEMPLATE_SHOW_OPTIONS = false; // Custom Spawn Template options. + [BDAPersistentSettingsField] public static bool CUSTOM_SPAWN_TEMPLATE_REPLACE_TEAM = false; // Replace all vessels on the team. // Time override settings [BDAPersistentSettingsField] public static bool TIME_OVERRIDE = false; // Enable the time control slider. [BDAPersistentSettingsField] public static float TIME_SCALE = 1f; // Time scale factor (higher speeds up the game rate without adjusting the physics time-step). + [BDAPersistentSettingsField] public static float TIME_SCALE_MAX = 10f; // Max time scale factor (to allow users to set custom max values). // Scoring categories [BDAPersistentSettingsField] public static float SCORING_HEADSHOT = 3; // Head-Shot Time Limit @@ -317,8 +379,14 @@ public class BDArmorySettings [BDAPersistentSettingsField] public static int EVOLUTION_HEATS_PER_GROUP = 1; [BDAPersistentSettingsField] public static bool AUTO_RESUME_EVOLUTION = false; // Automatically load the game and start evolution with the last used settings/seeds. Note: this overrides the AUTO_RESUME_TOURNAMENT setting. - // Countermeasure constants + // Missile & Countermeasure Settings + [BDAPersistentSettingsField] public static bool MISSILE_CM_SETTING_TOGGLE = false; + [BDAPersistentSettingsField] public static bool VARIABLE_MISSILE_VISIBILITY = false; //missile visual detection range dependant on boost/cruise/post-thrust state + [BDAPersistentSettingsField] public static bool ASPECTED_IR_SEEKERS = false; //IR Missiles will be subject to thermal occlusion mechanic + [BDAPersistentSettingsField] public static bool DUMB_IR_SEEKERS = false; // IR missiles will go after hottest thing they can see [BDAPersistentSettingsField] public static float FLARE_FACTOR = 1.6f; // Change this to make flares more or less effective, values close to or below 1.0 will cause flares to fail to decoy often [BDAPersistentSettingsField] public static float CHAFF_FACTOR = 0.65f; // Change this to make chaff more or less effective. Higher values will make chaff batter, lower values will make chaff worse. + [BDAPersistentSettingsField] public static float SMOKE_DEFLECTION_FACTOR = 10f; + [BDAPersistentSettingsField] public static int APS_THRESHOLD = 60; // Threshold caliber that APS will register for intercepting hostile shells/rockets } } diff --git a/BDArmory/UI/BDInputSettingsFields.cs b/BDArmory/Settings/BDInputSettingsFields.cs similarity index 90% rename from BDArmory/UI/BDInputSettingsFields.cs rename to BDArmory/Settings/BDInputSettingsFields.cs index 403ad15cd..db62e2207 100644 --- a/BDArmory/UI/BDInputSettingsFields.cs +++ b/BDArmory/Settings/BDInputSettingsFields.cs @@ -1,12 +1,17 @@ using System.Reflection; -using BDArmory.Core; -namespace BDArmory.UI +using BDArmory.Utils; + +namespace BDArmory.Settings { public class BDInputSettingsFields { // Note: order here determines order in input settings GUI within each section (based on prefix). //MAIN public static BDInputInfo WEAP_FIRE_KEY = new BDInputInfo("mouse 0", "Fire"); + public static BDInputInfo WEAP_FIRE_MISSILE_KEY = new BDInputInfo("Fire Missile"); + public static BDInputInfo WEAP_NEXT_KEY = new BDInputInfo("Next Weapon"); + public static BDInputInfo WEAP_PREV_KEY = new BDInputInfo("Prev Weapon"); + public static BDInputInfo WEAP_TOGGLE_ARMED_KEY = new BDInputInfo("Toggle Armed"); //TGP public static BDInputInfo TGP_SLEW_RIGHT = new BDInputInfo("[6]", "Slew Right"); @@ -23,6 +28,7 @@ public class BDInputSettingsFields public static BDInputInfo TGP_COM = new BDInputInfo("CoM-Track"); public static BDInputInfo TGP_NV = new BDInputInfo("Toggle NV"); public static BDInputInfo TGP_RESET = new BDInputInfo("Reset"); + public static BDInputInfo TGP_SELECT_NEXT_GPS_TARGET = new BDInputInfo("Select Next GPS Target"); //RADAR public static BDInputInfo RADAR_LOCK = new BDInputInfo("Lock/Unlock"); diff --git a/BDArmory/UI/BDTISettings.cs b/BDArmory/Settings/BDTISettings.cs similarity index 97% rename from BDArmory/UI/BDTISettings.cs rename to BDArmory/Settings/BDTISettings.cs index 1c430b70e..4fc897188 100644 --- a/BDArmory/UI/BDTISettings.cs +++ b/BDArmory/Settings/BDTISettings.cs @@ -1,6 +1,6 @@ using System.IO; -namespace BDArmory.UI +namespace BDArmory.Settings { public class BDTISettings { diff --git a/BDArmory/UI/BDTISettingsFields.cs b/BDArmory/Settings/BDTISettingsFields.cs similarity index 92% rename from BDArmory/UI/BDTISettingsFields.cs rename to BDArmory/Settings/BDTISettingsFields.cs index c97b1b2aa..f6b09b692 100644 --- a/BDArmory/UI/BDTISettingsFields.cs +++ b/BDArmory/Settings/BDTISettingsFields.cs @@ -3,9 +3,11 @@ using System.Reflection; using UniLinq; using UnityEngine; -using BDArmory.Misc; -namespace BDArmory.UI +using BDArmory.UI; +using BDArmory.Utils; + +namespace BDArmory.Settings { [AttributeUsage(AttributeTargets.Field)] public class SettingsDataField : Attribute @@ -71,14 +73,14 @@ public static void Load() ConfigNode colors = fileNode.GetNode("TeamColors"); for (int i = 0; i < colors.CountValues; i++) { - Debug.Log("[TEAMICONS] loading team " + colors.values[i].name + "; color: " + Utils.ParseColor255(colors.values[i].value)); + Debug.Log("[TEAMICONS] loading team " + colors.values[i].name + "; color: " + GUIUtils.ParseColor255(colors.values[i].value)); if (BDTISetup.Instance.ColorAssignments.ContainsKey(colors.values[i].name)) { - BDTISetup.Instance.ColorAssignments[colors.values[i].name] = Utils.ParseColor255(colors.values[i].value); + BDTISetup.Instance.ColorAssignments[colors.values[i].name] = GUIUtils.ParseColor255(colors.values[i].value); } else { - BDTISetup.Instance.ColorAssignments.Add(colors.values[i].name, Utils.ParseColor255(colors.values[i].value)); + BDTISetup.Instance.ColorAssignments.Add(colors.values[i].name, GUIUtils.ParseColor255(colors.values[i].value)); } } } diff --git a/BDArmory/Parts/GPSTargetInfo.cs b/BDArmory/Targeting/GPSTargetInfo.cs similarity index 94% rename from BDArmory/Parts/GPSTargetInfo.cs rename to BDArmory/Targeting/GPSTargetInfo.cs index dd87f65e7..069fb9968 100644 --- a/BDArmory/Parts/GPSTargetInfo.cs +++ b/BDArmory/Targeting/GPSTargetInfo.cs @@ -1,7 +1,8 @@ using System; -using BDArmory.Misc; -namespace BDArmory.Parts +using BDArmory.Utils; + +namespace BDArmory.Targeting { [Serializable] public struct GPSTargetInfo diff --git a/BDArmory/Modules/ModuleTargetingCamera.cs b/BDArmory/Targeting/ModuleTargetingCamera.cs similarity index 92% rename from BDArmory/Modules/ModuleTargetingCamera.cs rename to BDArmory/Targeting/ModuleTargetingCamera.cs index 3912fc0de..d794dba3b 100644 --- a/BDArmory/Modules/ModuleTargetingCamera.cs +++ b/BDArmory/Targeting/ModuleTargetingCamera.cs @@ -1,17 +1,18 @@ using System.Collections; -using System.Collections.Generic; -using BDArmory.Core; -using BDArmory.Core.Extension; -using BDArmory.Core.Utils; +using UnityEngine; +using Debug = UnityEngine.Debug; + +using BDArmory.Control; using BDArmory.CounterMeasure; -using BDArmory.Misc; -using BDArmory.Parts; +using BDArmory.Extensions; using BDArmory.Radar; +using BDArmory.Settings; using BDArmory.UI; -using UnityEngine; -using Debug = UnityEngine.Debug; +using BDArmory.Utils; +using BDArmory.Weapons; +using BDArmory.Weapons.Missiles; -namespace BDArmory.Modules +namespace BDArmory.Targeting { public class ModuleTargetingCamera : PartModule { @@ -347,7 +348,7 @@ IEnumerator DelayedEnableRoutine() delayedEnabling = true; Vector3d savedGTP = bodyRelativeGTP; - Debug.Log("[BDArmory.ModuleTargetingCamera]: saved gtp: " + Utils.FormattedGeoPos(savedGTP, true)); + Debug.Log("[BDArmory.ModuleTargetingCamera]: saved gtp: " + BodyUtils.FormattedGeoPos(savedGTP, true)); Debug.Log("[BDArmory.ModuleTargetingCamera]: groundStabilized: " + groundStabilized); while (TargetingCamera.Instance == null) @@ -464,7 +465,7 @@ void Update() if (eyeHolderTransform) { - Vector3 projectedForward = Vector3.ProjectOnPlane(cameraParentTransform.forward, eyeHolderTransform.parent.up); + Vector3 projectedForward = cameraParentTransform.forward.ProjectOnPlanePreNormalized(eyeHolderTransform.parent.up); if (projectedForward != Vector3.zero) { eyeHolderTransform.rotation = Quaternion.LookRotation(projectedForward, eyeHolderTransform.parent.up); @@ -684,34 +685,34 @@ void OnGUI() if (activeCam == this && TargetingCamera.ReadyForUse) { BDArmorySetup.WindowRectTargetingCam = GUI.Window(125452, BDArmorySetup.WindowRectTargetingCam, WindowTargetCam, "Target Camera", GUI.skin.window); - BDGUIUtils.UseMouseEventInRect(BDArmorySetup.WindowRectTargetingCam); + GUIUtils.UseMouseEventInRect(BDArmorySetup.WindowRectTargetingCam); } //locked target icon if (groundStabilized) { - BDGUIUtils.DrawTextureOnWorldPos(groundTargetPosition, BDArmorySetup.Instance.greenPointCircleTexture, new Vector3(20, 20), 0); + GUIUtils.DrawTextureOnWorldPos(groundTargetPosition, BDArmorySetup.Instance.greenPointCircleTexture, new Vector3(20, 20), 0); } else { - BDGUIUtils.DrawTextureOnWorldPos(targetPointPosition, BDArmorySetup.Instance.greenCircleTexture, new Vector3(18, 18), 0); + GUIUtils.DrawTextureOnWorldPos(targetPointPosition, BDArmorySetup.Instance.greenCircleTexture, new Vector3(18, 18), 0); } } - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_RADAR) { GUI.Label(new Rect(600, 1000, 100, 100), "Slew rate: " + finalSlewSpeed); } - if (BDArmorySettings.DRAW_DEBUG_LINES) + if (BDArmorySettings.DEBUG_LINES && cameraEnabled && cameraParentTransform is not null) { if (groundStabilized) { - BDGUIUtils.DrawLineBetweenWorldPositions(cameraParentTransform.position, groundTargetPosition, 2, Color.red); + GUIUtils.DrawLineBetweenWorldPositions(cameraParentTransform.position, groundTargetPosition, 2, Color.red); } else { - BDGUIUtils.DrawLineBetweenWorldPositions(cameraParentTransform.position, targetPointPosition, 2, Color.red); + GUIUtils.DrawLineBetweenWorldPositions(cameraParentTransform.position, targetPointPosition, 2, Color.red); } } } @@ -782,8 +783,8 @@ void WindowTargetCam(int windowID) //horizon indicator float horizY = imageRect.y + imageRect.height - indicatorSize - indicatorBorder; - Vector3 hForward = Vector3.ProjectOnPlane(vesForward, upDirection); - float hAngle = -Utils.SignedAngle(hForward, vesForward, upDirection); + Vector3 hForward = vesForward.ProjectOnPlanePreNormalized(upDirection); + float hAngle = -BDAMath.SignedAngle(hForward, vesForward, upDirection); horizY -= (hAngle / 90) * (indicatorSize / 2); Rect horizonRect = new Rect(indicatorBorder + imageRect.x, horizY, indicatorSize, indicatorSize); GUI.DrawTexture(horizonRect, BDArmorySetup.Instance.horizonIndicatorTexture, ScaleMode.StretchToFill, true); @@ -792,14 +793,14 @@ void WindowTargetCam(int windowID) Rect rollRect = new Rect(indicatorBorder + imageRect.x, imageRect.y + imageRect.height - indicatorSize - indicatorBorder, indicatorSize, indicatorSize); GUI.DrawTexture(rollRect, rollReferenceTexture, ScaleMode.StretchToFill, true); Vector3 localUp = vessel.ReferenceTransform.InverseTransformDirection(upDirection); - localUp = Vector3.ProjectOnPlane(localUp, Vector3.up).normalized; - float rollAngle = -Utils.SignedAngle(-Vector3.forward, localUp, Vector3.right); + localUp = localUp.ProjectOnPlanePreNormalized(Vector3.up).normalized; + float rollAngle = -BDAMath.SignedAngle(-Vector3.forward, localUp, Vector3.right); GUIUtility.RotateAroundPivot(rollAngle, rollRect.center); GUI.DrawTexture(rollRect, rollIndicatorTexture, ScaleMode.StretchToFill, true); GUI.matrix = Matrix4x4.identity; //target direction indicator - float angleToTarget = Utils.SignedAngle(hForward, Vector3.ProjectOnPlane(targetPointPosition - transform.position, upDirection), Vector3.Cross(upDirection, hForward)); + float angleToTarget = BDAMath.SignedAngle(hForward, (targetPointPosition - transform.position).ProjectOnPlanePreNormalized(upDirection), Vector3.Cross(upDirection, hForward)); GUIUtility.RotateAroundPivot(angleToTarget, rollRect.center); GUI.DrawTexture(rollRect, BDArmorySetup.Instance.targetDirectionTexture, ScaleMode.StretchToFill, true); GUI.matrix = Matrix4x4.identity; @@ -807,7 +808,7 @@ void WindowTargetCam(int windowID) //resizing Rect resizeRect = new Rect(BDArmorySetup.WindowRectTargetingCam.width - 18, BDArmorySetup.WindowRectTargetingCam.height - 18, 16, 16); - GUI.DrawTexture(resizeRect, Utils.resizeTexture, ScaleMode.StretchToFill, true); + GUI.DrawTexture(resizeRect, GUIUtils.resizeTexture, ScaleMode.StretchToFill, true); if (Event.current.type == EventType.MouseDown && resizeRect.Contains(Event.current.mousePosition)) { ResizingWindow = true; @@ -823,7 +824,7 @@ void WindowTargetCam(int windowID) } } //ResetZoomKeys(); - BDGUIUtils.RepositionWindow(ref BDArmorySetup.WindowRectTargetingCam); + GUIUtils.RepositionWindow(ref BDArmorySetup.WindowRectTargetingCam); } internal static void UpdateTargetScale(float diff) @@ -947,7 +948,7 @@ private void DrawSideControlButtons(Rect imageRect) //geo data dataStyle.fontSize = (int)Mathf.Clamp(12 * BDArmorySettings.TARGET_WINDOW_SCALE, 8, 12); Rect geoRect = new Rect(imageRect.x, (adjCamImageSize * 0.94f), adjCamImageSize, 14); - string geoLabel = Utils.FormattedGeoPos(bodyRelativeGTP, false); + string geoLabel = BodyUtils.FormattedGeoPos(bodyRelativeGTP, false); GUI.Label(geoRect, geoLabel, dataStyle); //target data @@ -966,10 +967,10 @@ private void DrawSideControlButtons(Rect imageRect) //azimuth and elevation indicator //UNFINISHED /* - Vector2 azielPos = TargetAzimuthElevationScreenPos(imageRect, groundTargetPosition, 4); - Rect azielRect = new Rect(azielPos.x, azielPos.y, 4, 4); - GUI.DrawTexture(azielRect, BDArmorySetup.Instance.whiteSquareTexture, ScaleMode.StretchToFill, true); - */ + Vector2 azielPos = TargetAzimuthElevationScreenPos(imageRect, groundTargetPosition, 4); + Rect azielRect = new Rect(azielPos.x, azielPos.y, 4, 4); + GUI.DrawTexture(azielRect, BDArmorySetup.Instance.whiteSquareTexture, ScaleMode.StretchToFill, true); + */ //DLZ if (weaponManager && weaponManager.selectedWeapon != null) @@ -994,29 +995,29 @@ private void DrawSideControlButtons(Rect imageRect) float dlzX = 0; - BDGUIUtils.DrawRectangle(new Rect(0, 0, dlzWidth, dlzRect.height), Color.black); + GUIUtils.DrawRectangle(new Rect(0, 0, dlzWidth, dlzRect.height), Color.black); Rect maxRangeVertLineRect = new Rect(dlzRect.width - lineWidth, Mathf.Clamp(dlzRect.height - (dlz.maxLaunchRange * rangeToPixels), 0, dlzRect.height), lineWidth, Mathf.Clamp(dlz.maxLaunchRange * rangeToPixels, 0, dlzRect.height)); - BDGUIUtils.DrawRectangle(maxRangeVertLineRect, Color.white); + GUIUtils.DrawRectangle(maxRangeVertLineRect, Color.white); Rect maxRangeTickRect = new Rect(dlzX, maxRangeVertLineRect.y, dlzWidth, lineWidth); - BDGUIUtils.DrawRectangle(maxRangeTickRect, Color.white); + GUIUtils.DrawRectangle(maxRangeTickRect, Color.white); Rect minRangeTickRect = new Rect(dlzX, Mathf.Clamp(dlzRect.height - (dlz.minLaunchRange * rangeToPixels), 0, dlzRect.height), dlzWidth, lineWidth); - BDGUIUtils.DrawRectangle(minRangeTickRect, Color.white); + GUIUtils.DrawRectangle(minRangeTickRect, Color.white); Rect rTrTickRect = new Rect(dlzX, Mathf.Clamp(dlzRect.height - (dlz.rangeTr * rangeToPixels), 0, dlzRect.height), dlzWidth, lineWidth); - BDGUIUtils.DrawRectangle(rTrTickRect, Color.white); + GUIUtils.DrawRectangle(rTrTickRect, Color.white); Rect noEscapeLineRect = new Rect(dlzX, rTrTickRect.y, lineWidth, minRangeTickRect.y - rTrTickRect.y); - BDGUIUtils.DrawRectangle(noEscapeLineRect, Color.white); + GUIUtils.DrawRectangle(noEscapeLineRect, Color.white); GUI.EndGroup(); @@ -1024,7 +1025,7 @@ private void DrawSideControlButtons(Rect imageRect) float targetDistY = dlzRect.y + dlzRect.height - (targetRange * rangeToPixels); Rect targetDistanceRect = new Rect(dlzRect.x - (targetDistIconSize / 2), targetDistY, (targetDistIconSize / 2) + dlzRect.width, targetDistIconSize); - BDGUIUtils.DrawRectangle(targetDistanceRect, Color.white); + GUIUtils.DrawRectangle(targetDistanceRect, Color.white); } } } @@ -1300,7 +1301,7 @@ public void GroundStabilize() RaycastHit rayHit; Ray ray = new Ray(cameraParentTransform.position + (50 * cameraParentTransform.forward), cameraParentTransform.forward); - bool raycasted = Physics.Raycast(ray, out rayHit, maxRayDistance - 50, (int)(LayerMasks.Parts | LayerMasks.Scenery | LayerMasks.EVA | LayerMasks.Unknown19 | LayerMasks.Unknown23)); + bool raycasted = Physics.Raycast(ray, out rayHit, maxRayDistance - 50, (int)(LayerMasks.Parts | LayerMasks.Scenery | LayerMasks.EVA | LayerMasks.Unknown19 | LayerMasks.Unknown23 | LayerMasks.Wheels)); if (raycasted) { if (FlightGlobals.getAltitudeAtPos(rayHit.point) < 0) @@ -1309,14 +1310,28 @@ public void GroundStabilize() } else { + KerbalEVA hitEVA = rayHit.collider.gameObject.GetComponentUpwards(); + Part p = hitEVA ? hitEVA.part : rayHit.collider.GetComponentInParent(); + + bool pCheck = false; + + if (p && p.vessel) + { + var pMissile = VesselModuleRegistry.GetModule(p.vessel); + if (pMissile != null) + { + if (pMissile.SourceVessel == vessel) return; + } + pCheck = true; + } + groundStabilized = true; groundTargetPosition = rayHit.point; if (CoMLock) { - KerbalEVA hitEVA = rayHit.collider.gameObject.GetComponentUpwards(); - Part p = hitEVA ? hitEVA.part : rayHit.collider.GetComponentInParent(); - if (p && p.vessel && p.vessel.CoM != Vector3.zero) + + if (pCheck && p.vessel.CoM != Vector3.zero) { groundTargetPosition = p.vessel.CoM + (p.vessel.Velocity() * Time.fixedDeltaTime); StartCoroutine(StabilizeNextFrame()); @@ -1353,7 +1368,7 @@ public void GroundStabilize() } } - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_RADAR) { MoveDebugSphere(); } @@ -1376,7 +1391,7 @@ void GetHitPoint() RaycastHit rayHit; Ray ray = new Ray(cameraParentTransform.position + (50 * cameraParentTransform.forward), cameraParentTransform.forward); - if (Physics.Raycast(ray, out rayHit, maxRayDistance - 50, (int)(LayerMasks.Parts | LayerMasks.Scenery | LayerMasks.EVA | LayerMasks.Unknown19 | LayerMasks.Unknown23))) + if (Physics.Raycast(ray, out rayHit, maxRayDistance - 50, (int)(LayerMasks.Parts | LayerMasks.Scenery | LayerMasks.EVA | LayerMasks.Unknown19 | LayerMasks.Unknown23 | LayerMasks.Wheels))) { targetPointPosition = rayHit.point; @@ -1475,13 +1490,22 @@ public IEnumerator PointToPositionRoutine(Vector3 position) radarLock = false; StopResetting(); ClearTarget(); - if (cameraParentTransform == null) yield break; + if (cameraParentTransform == null) + { + slewingToPosition = false; + yield break; + } while (!stopPTPR && Vector3.Angle(cameraParentTransform.transform.forward, position - (cameraParentTransform.transform.position)) > 0.1f) { Vector3 newForward = Vector3.RotateTowards(cameraParentTransform.transform.forward, position - cameraParentTransform.transform.position, 90 * Mathf.Deg2Rad * Time.fixedDeltaTime, 0); //cameraParentTransform.rotation = Quaternion.LookRotation(newForward, VectorUtils.GetUpDirection(transform.position)); PointCameraModel(newForward); yield return new WaitForFixedUpdate(); + if (cameraParentTransform == null) + { + slewingToPosition = false; + yield break; + } if (gimbalLimitReached) { ClearTarget(); @@ -1497,7 +1521,6 @@ public IEnumerator PointToPositionRoutine(Vector3 position) GroundStabilize(); } slewingToPosition = false; - yield break; } void StopResetting() @@ -1511,7 +1534,7 @@ void StopResetting() void ParseFovs() { - zoomFovs = Utils.ParseToFloatArray(zoomFOVs); + zoomFovs = OtherUtils.ParseToFloatArray(zoomFOVs); } void OnDestroy() @@ -1526,16 +1549,15 @@ void OnDestroy() weaponManager.slavingTurrets = false; } } - - GameEvents.onVesselCreate.Remove(Disconnect); } + GameEvents.onVesselCreate.Remove(Disconnect); } Vector2 TargetAzimuthElevationScreenPos(Rect screenRect, Vector3 targetPosition, float textureSize) { Vector3 localPos = vessel.ReferenceTransform.InverseTransformPoint(targetPosition); Vector3 aziRef = Vector3.up; - Vector3 aziPos = Vector3.ProjectOnPlane(localPos, Vector3.forward); + Vector3 aziPos = localPos.ProjectOnPlanePreNormalized(Vector3.forward); float elevation = VectorUtils.SignedAngle(aziPos, localPos, Vector3.forward); float normElevation = elevation / 70; diff --git a/BDArmory/Parts/TGPCamRotator.cs b/BDArmory/Targeting/TGPCamRotator.cs similarity index 90% rename from BDArmory/Parts/TGPCamRotator.cs rename to BDArmory/Targeting/TGPCamRotator.cs index b11e8fa14..e70b85b1d 100644 --- a/BDArmory/Parts/TGPCamRotator.cs +++ b/BDArmory/Targeting/TGPCamRotator.cs @@ -1,6 +1,6 @@ using UnityEngine; -namespace BDArmory.Parts +namespace BDArmory.Targeting { public class TGPCamRotator : MonoBehaviour { diff --git a/BDArmory/Parts/TGPCameraEffects.cs b/BDArmory/Targeting/TGPCameraEffects.cs similarity index 94% rename from BDArmory/Parts/TGPCameraEffects.cs rename to BDArmory/Targeting/TGPCameraEffects.cs index 8c1ce05ee..675865d06 100644 --- a/BDArmory/Parts/TGPCameraEffects.cs +++ b/BDArmory/Targeting/TGPCameraEffects.cs @@ -1,8 +1,9 @@ -using BDArmory.Core; -using BDArmory.Shaders; using UnityEngine; -namespace BDArmory.Parts +using BDArmory.Settings; +using BDArmory.Shaders; + +namespace BDArmory.Targeting { public class TGPCameraEffects : MonoBehaviour { diff --git a/BDArmory/Targeting/TargetInfo.cs b/BDArmory/Targeting/TargetInfo.cs index 80b46133b..2af236cae 100644 --- a/BDArmory/Targeting/TargetInfo.cs +++ b/BDArmory/Targeting/TargetInfo.cs @@ -1,13 +1,18 @@ using System.Collections; using System.Collections.Generic; using System.Linq; -using BDArmory.Core; -using BDArmory.Core.Extension; -using BDArmory.Misc; -using BDArmory.Modules; -using BDArmory.UI; using UnityEngine; +using BDArmory.Competition; +using BDArmory.Control; +using BDArmory.Extensions; +using BDArmory.Settings; +using BDArmory.UI; +using BDArmory.Utils; +using BDArmory.Weapons; +using BDArmory.Weapons.Missiles; +using BDArmory.WeaponMounts; + namespace BDArmory.Targeting { public class TargetInfo : MonoBehaviour @@ -17,20 +22,28 @@ public class TargetInfo : MonoBehaviour public MissileBase MissileBaseModule; public MissileFire weaponManager; Dictionary> friendliesEngaging = new Dictionary>(); + public Dictionary detected = new Dictionary(); public Dictionary detectedTime = new Dictionary(); public float radarBaseSignature = -1; public bool radarBaseSignatureNeedsUpdate = true; + public float radarRCSReducedSignature; public float radarModifiedSignature; - public float radarLockbreakFactor; + public float radarLockbreakFactor = 1; public float radarJammingDistance; public bool alreadyScheduledRCSUpdate = false; public float radarMassAtUpdate = 0f; - - public List targetWeaponList = new List(); - public List targetEngineList = new List(); - public List targetCommandList = new List(); - public List targetMassList = new List(); + public Vector3 bounds = Vector3.zero; + + public bool targetPartListNeedsUpdating = true; // Only update when needed — avoids excessive calling due to events. + public List targetWeaponList { get { if (targetPartListNeedsUpdating) UpdateTargetPartList(); return _targetWeaponList; } } + public List targetEngineList { get { if (targetPartListNeedsUpdating) UpdateTargetPartList(); return _targetEngineList; } } + public List targetCommandList { get { if (targetPartListNeedsUpdating) UpdateTargetPartList(); return _targetCommandList; } } + public List targetMassList { get { if (targetPartListNeedsUpdating) UpdateTargetPartList(); return _targetMassList; } } + public List _targetWeaponList = new List(); + List _targetEngineList = new List(); + List _targetCommandList = new List(); + public List _targetMassList = new List(); public bool isLandedOrSurfaceSplashed { @@ -160,6 +173,7 @@ public bool isThreat return false; } } + void Awake() { if (!vessel) @@ -214,7 +228,7 @@ void Awake() GameEvents.onVesselPartCountChanged.Add(VesselModified); //massRoutine = StartCoroutine(MassRoutine()); // TODO: CHECK BEHAVIOUR AND SIDE EFFECTS! } - UpdateTargetPartList(); + targetPartListNeedsUpdating = true; GameEvents.onVesselDestroy.Add(CleanFriendliesEngaging); } @@ -227,7 +241,7 @@ void OnDestroy() { //remove delegate from peace enable event BDArmorySetup.OnPeaceEnabled -= OnPeaceEnabled; - vessel.OnJustAboutToBeDestroyed -= AboutToBeDestroyed; + if (vessel is not null) vessel.OnJustAboutToBeDestroyed -= AboutToBeDestroyed; GameEvents.onVesselPartCountChanged.Remove(VesselModified); GameEvents.onVesselDestroy.Remove(CleanFriendliesEngaging); BDATargetManager.RemoveTarget(this); @@ -238,17 +252,17 @@ IEnumerator UpdateRCSDelayed() if (radarMassAtUpdate > 0) { float massPercentageDifference = (radarMassAtUpdate - vessel.GetTotalMass()) / radarMassAtUpdate; - if ((massPercentageDifference > 0.025f) && (weaponManager) && (weaponManager.missilesAway == 0) && !weaponManager.guardFiringMissile) + if ((massPercentageDifference > 0.025f) && (weaponManager) && (weaponManager.missilesAway.Count == 0) && !weaponManager.guardFiringMissile) { alreadyScheduledRCSUpdate = true; yield return new WaitForSeconds(1.0f); // Wait for any explosions to finish radarBaseSignatureNeedsUpdate = true; // Update RCS if vessel mass changed by more than 2.5% after a part was lost - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.TargetInfo]: RCS mass update triggered for " + vessel.vesselName + ", difference: " + (massPercentageDifference * 100f).ToString("0.0")); + if (BDArmorySettings.DEBUG_RADAR) Debug.Log("[BDArmory.TargetInfo]: RCS mass update triggered for " + vessel.vesselName + ", difference: " + (massPercentageDifference * 100f).ToString("0.0")); } } } - void Update() + void FixedUpdate() { if (vessel == null) { @@ -266,51 +280,38 @@ void Update() public void UpdateTargetPartList() { - targetCommandList.Clear(); - targetWeaponList.Clear(); - targetMassList.Clear(); - targetEngineList.Clear(); + targetPartListNeedsUpdating = false; + _targetCommandList.Clear(); + _targetWeaponList.Clear(); + _targetMassList.Clear(); + _targetEngineList.Clear(); //anything else? fueltanks? - could be useful if incindiary ammo gets implemented //power generation? - radiators/generators - if doing CoaDE style fights/need reactors to power weapons if (vessel == null) return; - using (List.Enumerator part = vessel.Parts.GetEnumerator()) - while (part.MoveNext()) - { - if (part.Current == null) continue; - - if (part.Current.FindModuleImplementing() || part.Current.FindModuleImplementing()) - { - targetWeaponList.Add(part.Current); - } - if (part.Current.FindModuleImplementing() || part.Current.FindModuleImplementing()) - { - targetEngineList.Add(part.Current); - } - - if (part.Current.FindModuleImplementing() || part.Current.FindModuleImplementing()) - { - targetCommandList.Add(part.Current); - } - targetMassList.Add(part.Current); - } - targetMassList = targetMassList.OrderBy(w => w.mass).ToList(); //weight target part priority by part mass, also serves as a default 'target heaviest part' in case other options not selected - targetMassList.Reverse(); //Order by mass is lightest to heaviest. We want H>L - if (targetMassList.Count > 10) - targetMassList.RemoveRange(10, (targetMassList.Count - 10)); //trim to max turret targets - targetCommandList = targetCommandList.OrderBy(w => w.mass).ToList(); - targetCommandList.Reverse(); - if (targetCommandList.Count > 10) - targetCommandList.RemoveRange(10, (targetCommandList.Count - 10)); - targetEngineList = targetEngineList.OrderBy(w => w.mass).ToList(); - targetEngineList.Reverse(); - if (targetEngineList.Count > 10) - targetEngineList.RemoveRange(10, (targetEngineList.Count - 10)); - targetWeaponList = targetWeaponList.OrderBy(w => w.mass).ToList(); - targetWeaponList.Reverse(); - if (targetWeaponList.Count > 10) - targetWeaponList.RemoveRange(10, (targetWeaponList.Count - 10)); + bounds = vessel.GetBounds(); // Update vessel bounds on part changes + + // Get the parts via the VesselModuleRegistry to avoid the expensive Find... commands. + VesselModuleRegistry.OnVesselModified(vessel); // Make sure the vessel is up-to-date since this can happen as part of an event. + _targetWeaponList.AddUniqueRange(VesselModuleRegistry.GetModuleWeapons(vessel).Select(m => m.part).Concat(VesselModuleRegistry.GetModules(vessel).Select(m => m.part)).Where(p => p is not null)); + _targetEngineList.AddUniqueRange(VesselModuleRegistry.GetModuleEngines(vessel).Select(m => m.part).Concat(VesselModuleRegistry.GetModules(vessel).Select(m => m.part)).Where(p => p is not null)); + _targetCommandList.AddUniqueRange(VesselModuleRegistry.GetModuleCommands(vessel).Select(m => m.part).Concat(VesselModuleRegistry.GetKerbalSeats(vessel).Select(m => m.part)).Where(p => p is not null)); + _targetMassList.AddRange(vessel.Parts.Where(p => p is not null)); + + // Sort and cull target part lists. + _targetMassList.Sort((p1, p2) => (p2.mass.CompareTo(p1.mass))); // Heaviest to lightest. + if (_targetMassList.Count > 10) + _targetMassList.RemoveRange(10, (_targetMassList.Count - 10)); //trim to max turret targets + _targetCommandList.Sort((p1, p2) => (p2.mass.CompareTo(p1.mass))); + if (_targetCommandList.Count > 10) + _targetCommandList.RemoveRange(10, (_targetCommandList.Count - 10)); + _targetEngineList.Sort((p1, p2) => (p2.mass.CompareTo(p1.mass))); + if (_targetEngineList.Count > 10) + _targetEngineList.RemoveRange(10, (_targetEngineList.Count - 10)); + _targetWeaponList.Sort((p1, p2) => (p2.mass.CompareTo(p1.mass))); + if (_targetWeaponList.Count > 10) + _targetWeaponList.RemoveRange(10, (_targetWeaponList.Count - 10)); } void CleanFriendliesEngaging(Vessel v) @@ -400,7 +401,18 @@ public float TargetPriATA(MissileFire myMf) // Square cosine of antenna train an ataDot = (ataDot + 1) / 2; // Adjust from 0-1 instead of -1 to 1 return ataDot * ataDot; } - + public float TargetPriEngagement(MissileFire mf) // Differentiate between flying and surface targets + { + if (mf == null) return 0; // no WM, so no valid target, no impact on targeting score + if (mf.vessel.LandedOrSplashed) + { + return -1; //ground target + } + else + { + return 1; // Air target + } + } public float TargetPriAcceleration() // Normalized clamped acceleration for the target { float bodyGravity = (float)PhysicsGlobals.GravitationalAcceleration * (float)vessel.orbit.referenceBody.GeeASL; // Set gravity for calculations; @@ -466,7 +478,8 @@ public float TargetPriAoD(MissileFire myMF) if (myMF == null) return 0; var relativePosition = vessel.transform.position - myMF.vessel.transform.position; float theta = Vector3.Angle(myMF.vessel.srf_vel_direction, relativePosition); - return Mathf.Clamp(((Mathf.Pow(Mathf.Cos(theta / 2f), 2f) + 1f) * 100f / Mathf.Max(10f, relativePosition.magnitude)) / 2, 0, 1); // Ranges from 0 to 1, clamped at 1 for distances closer than 100m + float cosTheta2 = Mathf.Cos(theta / 2f); + return Mathf.Clamp(((cosTheta2 * cosTheta2 + 1f) * 100f / Mathf.Max(10f, relativePosition.magnitude)) / 2, 0, 1); // Ranges from 0 to 1, clamped at 1 for distances closer than 100m } public float TargetPriMass(MissileFire mf, MissileFire myMf) // Relative mass compared to our own mass @@ -484,6 +497,20 @@ public float TargetPriMass(MissileFire mf, MissileFire myMf) // Relative mass co } } + public float TargetPriDmg(MissileFire mf) // Relative HP of Target + { + if (mf == null) return 0; + if (mf.vessel != null) + { + float TargetPriDmg = 1 - Mathf.Clamp(mf.currentHP / mf.totalHP, 0, 1); //ranges from 0-1, 0 is undamaged, 1 is cockpit falling out of the sky + return TargetPriDmg; + } + else + { + return 0; + } + } + public float TargetPriProtectTeammate(MissileFire mf, MissileFire myMf) // If target is attacking one of our teammates. 1 if true, 0 if false. { if (myMf == null) return 0; @@ -526,7 +553,7 @@ public int TotalEngaging() int engaging = 0; using (var teamEngaging = friendliesEngaging.GetEnumerator()) while (teamEngaging.MoveNext()) - engaging += teamEngaging.Current.Value.Count; + engaging += teamEngaging.Current.Value.Count(wm => wm != null); return engaging; } @@ -568,11 +595,11 @@ public bool IsCloser(TargetInfo otherTarget, MissileFire myMf) public void VesselModified(Vessel v) { - if (v && v == this.vessel) + if (v && v == this.vessel && isActiveAndEnabled) { if (!alreadyScheduledRCSUpdate) StartCoroutine(UpdateRCSDelayed()); - UpdateTargetPartList(); + targetPartListNeedsUpdating = true; } } diff --git a/BDArmory/Targeting/TargetSignatureData.cs b/BDArmory/Targeting/TargetSignatureData.cs index fc4797e27..e383bb40a 100644 --- a/BDArmory/Targeting/TargetSignatureData.cs +++ b/BDArmory/Targeting/TargetSignatureData.cs @@ -1,12 +1,12 @@ using System; -using System.Collections.Generic; -using BDArmory.Core; -using BDArmory.Core.Extension; +using UnityEngine; + +using BDArmory.Competition; using BDArmory.CounterMeasure; -using BDArmory.Misc; -using BDArmory.Modules; +using BDArmory.Extensions; using BDArmory.Radar; -using UnityEngine; +using BDArmory.Settings; +using BDArmory.Utils; namespace BDArmory.Targeting { @@ -24,6 +24,7 @@ public struct TargetSignatureData : IEquatable public VesselECMJInfo vesselJammer; public ModuleRadar lockedByRadar; public Vessel vessel; + public Part IRSource; bool orbital; Orbit orbit; @@ -35,7 +36,7 @@ public bool Equals(TargetSignatureData other) timeAcquired == other.timeAcquired; } - public TargetSignatureData(Vessel v, float _signalStrength) + public TargetSignatureData(Vessel v, float _signalStrength, Part heatpart = null) { orbital = v.InOrbit(); orbit = v.orbit; @@ -43,8 +44,8 @@ public TargetSignatureData(Vessel v, float _signalStrength) timeAcquired = Time.time; vessel = v; velocity = v.Velocity(); - - geoPos = VectorUtils.WorldPositionToGeoCoords(v.CoM, v.mainBody); + IRSource = heatpart; + geoPos = VectorUtils.WorldPositionToGeoCoords(IRSource != null ? IRSource.transform.position : v.CoM, v.mainBody); acceleration = v.acceleration_immediate; exists = true; @@ -92,6 +93,7 @@ public TargetSignatureData(CMFlare flare, float _signalStrength) orbit = null; lockedByRadar = null; vessel = null; + IRSource = null; } public TargetSignatureData(Vector3 _velocity, Vector3 _position, Vector3 _acceleration, bool _exists, float _signalStrength) @@ -110,6 +112,7 @@ public TargetSignatureData(Vector3 _velocity, Vector3 _position, Vector3 _accele orbit = null; lockedByRadar = null; vessel = null; + IRSource = null; } public Vector3 position @@ -132,30 +135,40 @@ public Vector3 predictedPosition } } - public Vector3 predictedPositionWithChaffFactor + public Vector3 predictedPositionWithChaffFactor(float chaffEffectivity = 1f) { - get + // get chaff factor of vessel and calculate decoy distortion caused by chaff echos + float decoyFactor = 0f; + Vector3 posDistortion = Vector3.zero; + + if (vessel != null) { - // get chaff factor of vessel and calculate decoy distortion caused by chaff echos - float decoyFactor = 0f; - Vector3 posDistortion = Vector3.zero; + // chaff check + decoyFactor = (1f - RadarUtils.GetVesselChaffFactor(vessel)); - if (vessel != null) + if (decoyFactor > 0f) { - // chaff check - decoyFactor = (1f - RadarUtils.GetVesselChaffFactor(vessel)); - - if (decoyFactor > 0f) - { - // with ecm on better chaff effectiveness due to higher modifiedSignature - // higher speed -> missile decoyed further "behind" where the chaff drops (also means that for head-on engagements chaff is most like less effective!) - posDistortion = (vessel.GetSrfVelocity() * -1f * Mathf.Clamp(decoyFactor * decoyFactor, 0f, 0.5f)) + (UnityEngine.Random.insideUnitSphere * UnityEngine.Random.Range(targetInfo.radarModifiedSignature, targetInfo.radarModifiedSignature * targetInfo.radarModifiedSignature) * decoyFactor); - posDistortion *= Mathf.Max(BDArmorySettings.CHAFF_FACTOR, 0f); - } - } + // With ecm on better chaff effectiveness due to jammer strength + VesselECMJInfo vesseljammer = vessel.gameObject.GetComponent(); - return position + (velocity * age) + posDistortion; + // Jamming biases position distortion further to rear, depending on ratio of jamming strength and radarModifiedSignature + float jammingFactor = vesseljammer is null ? 0 : decoyFactor * Mathf.Clamp01(vesseljammer.jammerStrength / 100f / Mathf.Max(targetInfo.radarModifiedSignature, 0.1f)); + + // Random radius of distortion, 16-256m + float distortionFactor = decoyFactor * UnityEngine.Random.Range(16f, 256f); + + // Convert Float jammingFactor position bias and signatureFactor scaling to Vector3 position + Vector3 signatureDistortion = distortionFactor * (vessel.GetSrfVelocity().normalized * -1f * jammingFactor + UnityEngine.Random.insideUnitSphere); + + // Higher speed -> missile decoyed further "behind" where the chaff drops (also means that chaff is least effective for head-on engagements) + posDistortion = (vessel.GetSrfVelocity() * -1f * Mathf.Clamp(decoyFactor * decoyFactor, 0f, 0.5f)) + signatureDistortion; + + // Apply effects from global settings and individual missile chaffEffectivity + posDistortion *= Mathf.Max(BDArmorySettings.CHAFF_FACTOR, 0f) * chaffEffectivity; + } } + + return position + (velocity * age) + posDistortion; } public float altitude diff --git a/BDArmory/Parts/TargetingCamera.cs b/BDArmory/Targeting/TargetingCamera.cs similarity index 94% rename from BDArmory/Parts/TargetingCamera.cs rename to BDArmory/Targeting/TargetingCamera.cs index be5e0b80c..3bb614b6c 100644 --- a/BDArmory/Parts/TargetingCamera.cs +++ b/BDArmory/Targeting/TargetingCamera.cs @@ -1,9 +1,9 @@ -using System.Collections.Generic; -using BDArmory.Core; -using BDArmory.Modules; using UnityEngine; -namespace BDArmory.Parts +using BDArmory.Settings; +using BDArmory.Utils; + +namespace BDArmory.Targeting { public class TargetingCamera : MonoBehaviour { @@ -99,7 +99,8 @@ void VesselChange(Vessel v) using (var mtc = VesselModuleRegistry.GetModules(v).GetEnumerator()) while (mtc.MoveNext()) { - Debug.Log("[BDArmory.TargetingCamera]: Vessel switched to vessel with targeting camera. Refreshing camera state."); + if (mtc.Current is null) continue; + if (BDArmorySettings.DEBUG_RADAR) Debug.Log("[BDArmory.TargetingCamera]: Vessel switched to vessel with targeting camera. Refreshing camera state."); if (mtc.Current.cameraEnabled) { @@ -140,8 +141,8 @@ public void EnableCamera(Transform parentTransform) void RenderCameras() { - cameras[3].Render(); - cameras[2].Render(); + if (cameras[3] != null) cameras[3].Render(); + if (cameras[2] != null) cameras[2].Render(); Color origAmbientColor = RenderSettings.ambientLight; if (nvMode) @@ -149,8 +150,8 @@ void RenderCameras() RenderSettings.ambientLight = new Color(0.5f, 0.5f, 0.5f, 1); nvLight.enabled = true; } - cameras[1].Render(); - cameras[0].Render(); + if (cameras[1] != null) cameras[1].Render(); + if (cameras[0] != null) cameras[0].Render(); nvLight.enabled = false; if (nvMode) diff --git a/BDArmory/UI/BDAEditorAnalysisWindow.cs b/BDArmory/UI/BDAEditorAnalysisWindow.cs index 87e4ebe6c..d9f367458 100644 --- a/BDArmory/UI/BDAEditorAnalysisWindow.cs +++ b/BDArmory/UI/BDAEditorAnalysisWindow.cs @@ -1,12 +1,13 @@ using System; using System.Collections; using System.Collections.Generic; -using BDArmory.Misc; -using BDArmory.Modules; -using BDArmory.Radar; using KSP.UI.Screens; using UnityEngine; +using BDArmory.CounterMeasure; +using BDArmory.Radar; +using BDArmory.Utils; + namespace BDArmory.UI { [KSPAddon(KSPAddon.Startup.EditorAny, false)] @@ -72,22 +73,30 @@ private void FillRadarList() private void OnEditorShipModifiedEvent(ShipConstruct data) { + if (data is null) return; delayedTakeSnapShot = true; if (!delayedTakeSnapShotInProgress) - StartCoroutine(DelayedTakeSnapShot()); + StartCoroutine(DelayedTakeSnapShot(data)); } private bool delayedTakeSnapShot = false; private bool delayedTakeSnapShotInProgress = false; - IEnumerator DelayedTakeSnapShot() + IEnumerator DelayedTakeSnapShot(ShipConstruct ship) { delayedTakeSnapShotInProgress = true; + var wait = new WaitForFixedUpdate(); while (delayedTakeSnapShot) // Wait until ship modified events stop coming. { delayedTakeSnapShot = false; - yield return null; - yield return null; // Two yield nulls to wait for HP changes to delayed ship modified events in HitpointTracker + yield return wait; } + yield return new WaitUntilFixed(() => + ship == null || ship.Parts == null || ship.Parts.TrueForAll(p => + { + if (p == null) return true; + var hp = p.GetComponent(); + return hp == null || hp.Ready; + })); // Wait for HP changes to delayed ship modified events in HitpointTracker delayedTakeSnapShotInProgress = false; takeSnapshot = true; previous_index = -1; @@ -108,10 +117,7 @@ private void OnDestroy() IEnumerator ToolbarButtonRoutine() { if (toolbarButton || (!HighLogic.LoadedSceneIsEditor)) yield break; - while (!ApplicationLauncher.Ready) - { - yield return null; - } + yield return new WaitUntil(() => ApplicationLauncher.Ready && BDArmorySetup.toolbarButtonAdded); // Wait until after the main BDA toolbar button. AddToolbarButton(); } @@ -147,7 +153,7 @@ void OnGUI() { if (showRcsWindow) { - windowRect = GUI.Window(this.GetInstanceID(), windowRect, WindowRcs, windowTitle, BDArmorySetup.BDGuiSkin.window); + windowRect = GUI.Window(GUIUtility.GetControlID(FocusType.Passive), windowRect, WindowRcs, windowTitle, BDArmorySetup.BDGuiSkin.window); } PreventClickThrough(); @@ -194,7 +200,7 @@ void WindowRcs(int windowID) FillRadarList(); GUIStyle listStyle = new GUIStyle(BDArmorySetup.BDGuiSkin.button); listStyle.fixedHeight = 18; //make list contents slightly smaller - radarBox = new BDGUIComboBox(new Rect(10, 350, 600, 20), new Rect(10, 350, 250, 20), radarBoxText, radarsGUI, 124, listStyle); + radarBox = new BDGUIComboBox(new Rect(10, 350, 450, 20), new Rect(10, 350, 450, 20), radarBoxText, radarsGUI, 124, listStyle); } int selected_index = radarBox.Show(); @@ -256,7 +262,7 @@ void WindowRcs(int windowID) previous_index = selected_index; GUI.DragWindow(); - BDGUIUtils.RepositionWindow(ref windowRect); + GUIUtils.RepositionWindow(ref windowRect); } void WindowRcsLegacy(int windowID) @@ -311,7 +317,7 @@ void WindowRcsLegacy(int windowID) FillRadarList(); GUIStyle listStyle = new GUIStyle(BDArmorySetup.BDGuiSkin.button); listStyle.fixedHeight = 18; //make list contents slightly smaller - radarBox = new BDGUIComboBox(new Rect(10, 350, 600, 20), new Rect(10, 350, 250, 20), radarBoxText, radarsGUI, 124, listStyle); + radarBox = new BDGUIComboBox(new Rect(10, 350, 450, 20), new Rect(10, 350, 450, 20), radarBoxText, radarsGUI, 124, listStyle); } int selected_index = radarBox.Show(); @@ -373,7 +379,7 @@ void WindowRcsLegacy(int windowID) previous_index = selected_index; GUI.DragWindow(); - BDGUIUtils.RepositionWindow(ref windowRect); + GUIUtils.RepositionWindow(ref windowRect); } void takeRadarSnapshot() @@ -408,7 +414,7 @@ void takeRadarSnapshot() parts.Dispose(); if (rcsCount > 0) - rcsReductionFactor = Mathf.Clamp((rcsReductionFactor * rcsCount), 0.0f, 1); //same formula as in VesselECMJInfo must be used here! + rcsReductionFactor = Mathf.Max((rcsReductionFactor * rcsCount), 0.0f); //same formula as in VesselECMJInfo must be used here! } /// diff --git a/BDArmory/UI/BDAEditorArmorWindow.cs b/BDArmory/UI/BDAEditorArmorWindow.cs index 72ba7eba3..f6ef8f000 100644 --- a/BDArmory/UI/BDAEditorArmorWindow.cs +++ b/BDArmory/UI/BDAEditorArmorWindow.cs @@ -1,13 +1,15 @@ using System.Collections; using System.Collections.Generic; -using BDArmory.Core; -using BDArmory.Core.Extension; -using BDArmory.Core.Module; -using BDArmory.Misc; -using KSP.Localization; +using System.Linq; using KSP.UI.Screens; using UnityEngine; +using BDArmory.Armor; +using BDArmory.Damage; +using BDArmory.Extensions; +using BDArmory.Settings; +using BDArmory.Utils; + namespace BDArmory.UI { [KSPAddon(KSPAddon.Startup.EditorAny, false)] @@ -17,8 +19,7 @@ internal class BDAEditorArmorWindow : MonoBehaviour private ApplicationLauncherButton toolbarButton = null; private bool showArmorWindow = false; - private bool showHullMenu = false; - private string windowTitle = Localizer.Format("#LOC_BDArmory_ArmorTool"); + private string windowTitle = StringUtils.Localize("#LOC_BDArmory_ArmorTool"); private Rect windowRect = new Rect(300, 150, 300, 350); private float lineHeight = 20; private float height = 20; @@ -27,8 +28,21 @@ internal class BDAEditorArmorWindow : MonoBehaviour private BDGUIComboBox armorBox; private int previous_index = -1; + private GUIContent[] hullGUI; + private GUIContent hullBoxText; + private BDGUIComboBox hullBox; + private int previous_mat = -1; + private float oldLines = -1; + + GUIStyle listStyle; + private float totalArmorMass; private float totalArmorCost; + private float totalLift; + private float totalLiftArea; + private float totalLiftStackRatio; + private float wingLoading; + private float WLRatio; private bool CalcArmor = false; private bool shipModifiedfromCalcArmor = false; private bool SetType = false; @@ -41,8 +55,14 @@ internal class BDAEditorArmorWindow : MonoBehaviour private float ArmorDuctility = 0.6f; private float ArmorDiffusivity = 237; private float ArmorMaxTemp = 993; + private float ArmorVfactor = 8.45001135e-07f; + private float ArmorMu1 = 0.656060636f; + private float ArmorMu2 = 1.20190930f; + private float ArmorMu3 = 1.77791929f; private float ArmorCost = 0; + private bool armorslist = false; + private bool hullslist = false; private float Thickness = 10; private bool useNumField = false; private float oldThickness = 10; @@ -50,16 +70,16 @@ internal class BDAEditorArmorWindow : MonoBehaviour private bool Visualizer = false; private bool HPvisualizer = false; private bool HullVisualizer = false; + private bool LiftVisualizer = false; private bool oldVisualizer = false; private bool oldHPvisualizer = false; private bool oldHullVisualizer = false; + private bool oldLiftVisualizer = false; private bool refreshVisualizer = false; private bool refreshHPvisualizer = false; private bool refreshHullvisualizer = true; - private bool isWood = false; - private bool isSteel = false; - private bool isAluminium = true; - private int hullmat = 2; + private bool refreshLiftvisualizer = false; + private string hullmat = "Aluminium"; private float steelValue = 1; private float armorValue = 1; @@ -80,6 +100,7 @@ void Start() {"Thickness", gameObject.AddComponent().Initialise(0, 10, 0, 1500) }, // FIXME should use maxThickness instead of 1500 here. }; GameEvents.onEditorShipModified.Add(OnEditorShipModifiedEvent); + /* var modifiedCaliber = (15) + (15) * (2f * 0.15f * 0.15f); float bulletEnergy = ProjectileUtils.CalculateProjectileEnergy(0.388f, 1109); float yieldStrength = modifiedCaliber * modifiedCaliber * Mathf.PI / 100f * 940 * 30; @@ -87,9 +108,13 @@ void Start() { yieldStrength *= 0.7f; } - float newCaliber = ProjectileUtils.CalculateDeformation(yieldStrength, bulletEnergy, 30, 1109, 1176, 7850, 0.19f, 0.8f); - steelValue = ProjectileUtils.CalculatePenetration(30, newCaliber, 0.388f, 1109, 0.15f, 7850, 940, 30, 0.8f, false); + float newCaliber = ProjectileUtils.CalculateDeformation(yieldStrength, bulletEnergy, 30, 1109, 1176, 7850, 0.19f, 0.8f, false); + */ + //steelValue = ProjectileUtils.CalculatePenetration(30, newCaliber, 0.388f, 1109, 0.15f, 7850, 940, 30, 0.8f, false); + steelValue = ProjectileUtils.CalculatePenetration(30, 1109, 0.388f, 0.8f); exploValue = 940 * 1.15f * 7.85f; + listStyle = new GUIStyle(BDArmorySetup.BDGuiSkin.button); + listStyle.fixedHeight = 18; //make list contents slightly smaller } private void FillArmorList() @@ -100,28 +125,62 @@ private void FillArmorList() GUIContent gui = new GUIContent(ArmorInfo.armors[i].name); armorGUI[i] = gui; } - armorBoxText = new GUIContent(); - armorBoxText.text = Localizer.Format("#LOC_BDArmory_ArmorSelect"); + armorBoxText.text = StringUtils.Localize("#LOC_BDArmory_ArmorSelect"); } + private void FillHullList() + { + hullGUI = new GUIContent[HullInfo.materials.Count]; + for (int i = 0; i < HullInfo.materials.Count; i++) + { + GUIContent gui = new GUIContent(HullInfo.materials[i].name); + hullGUI[i] = gui; + } + hullBoxText = new GUIContent(); + hullBoxText.text = StringUtils.Localize("#LOC_BDArmory_Armor_HullType"); + } private void OnEditorShipModifiedEvent(ShipConstruct data) { + if (data is null) return; delayedRefreshVisuals = true; if (!delayedRefreshVisualsInProgress) - StartCoroutine(DelayedRefreshVisuals()); + StartCoroutine(DelayedRefreshVisuals(data)); } private bool delayedRefreshVisuals = false; private bool delayedRefreshVisualsInProgress = false; - IEnumerator DelayedRefreshVisuals() + IEnumerator DelayedRefreshVisuals(ShipConstruct ship) { delayedRefreshVisualsInProgress = true; - while (delayedRefreshVisuals) // Wait until ship modified events stop coming. + var wait = new WaitForFixedUpdate(); + int count = 0, countLimit = 50; + while (delayedRefreshVisuals && ++count < countLimit) // Wait until ship modified events stop coming, or countLimit ticks. { delayedRefreshVisuals = false; - yield return null; - yield return null; // Two yield nulls to wait for HP changes to delayed ship modified events in HitpointTracker + yield return wait; + } + if (count == countLimit) Debug.LogWarning($"[BDArmory.BDAEditorArmorWindow]: Continuous stream of OnEditorShipModifiedEvents for over {countLimit} frames."); + count = 0; + yield return new WaitUntilFixed(() => ++count == countLimit || + ship == null || ship.Parts == null || ship.Parts.TrueForAll(p => + { + if (p == null) return true; + var hp = p.GetComponent(); + return hp == null || hp.Ready; + })); // Wait for HP changes to delayed ship modified events in HitpointTracker + if (count == countLimit) + { + string reason = ""; + if (ship != null && ship.Parts != null) + reason = string.Join("; ", ship.Parts.Select(p => + { + if (p == null) return null; + var hp = p.GetComponent(); + if (hp == null || hp.Ready) return null; + return hp; + }).Where(hp => hp != null).Select(hp => $"{hp.part.name}: {hp.Why}")); + //Debug.LogWarning($"[BDArmory.BDAEditorArmorWindow]: Ship HP failed to settle within {countLimit} frames.{(string.IsNullOrEmpty(reason) ? "" : $" {reason}")}"); } delayedRefreshVisualsInProgress = false; @@ -131,20 +190,24 @@ IEnumerator DelayedRefreshVisuals() { CalcArmor = true; } - if (Visualizer || HPvisualizer || HullVisualizer) + if (Visualizer || HPvisualizer || HullVisualizer || LiftVisualizer) { refreshVisualizer = true; refreshHPvisualizer = true; refreshHullvisualizer = true; + refreshLiftvisualizer = true; } shipModifiedfromCalcArmor = false; + CalculateArmorMass(); + if (!FerramAerospace.hasFAR) + CalculateTotalLift(); // Re-calculate lift and wing loading on armor change + //Debug.Log("[ArmorTool] Recalculating mass/lift"); } } private void OnDestroy() { GameEvents.onEditorShipModified.Remove(OnEditorShipModifiedEvent); - if (toolbarButton) { ApplicationLauncher.Instance.RemoveModApplication(toolbarButton); @@ -155,10 +218,7 @@ private void OnDestroy() IEnumerator ToolbarButtonRoutine() { if (toolbarButton || (!HighLogic.LoadedSceneIsEditor)) yield break; - while (!ApplicationLauncher.Ready) - { - yield return null; - } + yield return new WaitUntil(() => ApplicationLauncher.Ready && BDArmorySetup.toolbarButtonAdded); // Wait until after the main BDA toolbar button. AddToolbarButton(); } @@ -188,6 +248,7 @@ public void HideToolbarGUI() Visualizer = false; HPvisualizer = false; HullVisualizer = false; + LiftVisualizer = false; Visualize(); } @@ -198,7 +259,7 @@ void OnGUI() { if (showArmorWindow) { - windowRect = GUI.Window(this.GetInstanceID(), windowRect, WindowArmor, windowTitle, BDArmorySetup.BDGuiSkin.window); + windowRect = GUI.Window(GUIUtility.GetControlID(FocusType.Passive), windowRect, WindowArmor, windowTitle, BDArmorySetup.BDGuiSkin.window); } PreventClickThrough(); } @@ -224,13 +285,14 @@ void WindowArmor(int windowID) style.fontStyle = FontStyle.Normal; - if (GUI.Button(new Rect(10, line * lineHeight, 280, lineHeight), Localizer.Format("#LOC_BDArmory_ArmorHPVisualizer"), HPvisualizer ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button)) + if (GUI.Button(new Rect(10, line * lineHeight, 280, lineHeight), StringUtils.Localize("#LOC_BDArmory_ArmorHPVisualizer"), HPvisualizer ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button)) { HPvisualizer = !HPvisualizer; if (HPvisualizer) { Visualizer = false; HullVisualizer = false; + LiftVisualizer = false; } } line += 1.25f; @@ -238,13 +300,14 @@ void WindowArmor(int windowID) if (!BDArmorySettings.RESET_ARMOUR) { - if (GUI.Button(new Rect(10, line * lineHeight, 280, lineHeight), Localizer.Format("#LOC_BDArmory_ArmorVisualizer"), Visualizer ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button)) + if (GUI.Button(new Rect(10, line * lineHeight, 280, lineHeight), StringUtils.Localize("#LOC_BDArmory_ArmorVisualizer"), Visualizer ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button)) { Visualizer = !Visualizer; if (Visualizer) { HPvisualizer = false; HullVisualizer = false; + LiftVisualizer = false; } } line += 1.25f; @@ -252,26 +315,44 @@ void WindowArmor(int windowID) if (!BDArmorySettings.RESET_HULL) { - if (GUI.Button(new Rect(10, line * lineHeight, 280, lineHeight), Localizer.Format("#LOC_BDArmory_ArmorHullVisualizer"), HullVisualizer ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button)) + if (GUI.Button(new Rect(10, line * lineHeight, 280, lineHeight), StringUtils.Localize("#LOC_BDArmory_ArmorHullVisualizer"), HullVisualizer ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button)) { HullVisualizer = !HullVisualizer; if (HullVisualizer) { HPvisualizer = false; Visualizer = false; + LiftVisualizer = false; + } + } + line += 1.25f; + } + + if (!FerramAerospace.hasFAR) + { + if (GUI.Button(new Rect(10, line * lineHeight, 280, lineHeight), StringUtils.Localize("#LOC_BDArmory_ArmorLiftVisualizer"), LiftVisualizer ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button)) + { + LiftVisualizer = !LiftVisualizer; + if (LiftVisualizer) + { + Visualizer = false; + HullVisualizer = false; + HPvisualizer = false; } } - line += 1.5f; + line += 1.25f; } - if ((refreshHPvisualizer || HPvisualizer != oldHPvisualizer) || (refreshVisualizer || Visualizer != oldVisualizer) || (refreshHullvisualizer || HullVisualizer != oldHullVisualizer)) + line += 0.25f; + + if ((refreshHPvisualizer || HPvisualizer != oldHPvisualizer) || (refreshVisualizer || Visualizer != oldVisualizer) || (refreshHullvisualizer || HullVisualizer != oldHullVisualizer) || (refreshLiftvisualizer || LiftVisualizer != oldLiftVisualizer)) { Visualize(); } if (!BDArmorySettings.RESET_ARMOUR) { - GUI.Label(new Rect(10, line * lineHeight, 300, lineHeight), Localizer.Format("#LOC_BDArmory_ArmorThickness") + ": " + Thickness + "mm", style); + GUI.Label(new Rect(10, line * lineHeight, 300, lineHeight), $"{StringUtils.Localize("#LOC_BDArmory_ArmorThickness")}: {Thickness} mm", style); line++; if (!useNumField) { @@ -288,15 +369,42 @@ void WindowArmor(int windowID) line++; } line += 0.75f; - GUI.Label(new Rect(10, line * lineHeight, 300, lineHeight), Localizer.Format("#LOC_BDArmory_ArmorTotalMass") + ": " + totalArmorMass.ToString("0.00"), style); + GUI.Label(new Rect(10, line * lineHeight, 300, lineHeight), $"{StringUtils.Localize("#LOC_BDArmory_ArmorTotalMass")}: {totalArmorMass:0.00}", style); + line++; + GUI.Label(new Rect(10, line * lineHeight, 300, lineHeight), $"{StringUtils.Localize("#LOC_BDArmory_ArmorTotalCost")}: {Mathf.Round(totalArmorCost)}", style); line++; - GUI.Label(new Rect(10, line * lineHeight, 300, lineHeight), Localizer.Format("#LOC_BDArmory_ArmorTotalCost") + ": " + Mathf.Round(totalArmorCost), style); - line += 1.5f; + } + if (!FerramAerospace.hasFAR) + { + GUI.Label(new Rect(10, line * lineHeight, 300, lineHeight), $"{StringUtils.Localize("#LOC_BDArmory_ArmorTotalLift")}: {totalLift:0.00} ({totalLiftArea:F3} m2)", style); + line++; + GUI.Label(new Rect(10, line * lineHeight, 300, lineHeight), $"{StringUtils.Localize("#LOC_BDArmory_ArmorWingLoading")}: {wingLoading:0.0} ({WLRatio:F3} kg/m2)", style); + line++; + GUI.Label(new Rect(10, line * lineHeight, 300, lineHeight), $"{StringUtils.Localize("#LOC_BDArmory_ArmorLiftStacking")}: {totalLiftStackRatio:0%}", style); + line++; +#if DEBUG + if (GUI.Button(new Rect(10, line++ * lineHeight, 280, lineHeight), "Find Wings", BDArmorySetup.ButtonStyle)) + { + var wings = FindWings(); + foreach (var wing in wings) + { + Debug.Log($"DEBUG Wing: {string.Join(", ", wing.Select(w => $"{w.name}:{w.persistentId}"))}"); + } + // Total lift stacking is the combination of inter- and intra-wing lift stacking. + // Calculate inter-wing lift stacking by calculating stacking between wings. + var liftStacking = CalculateInterWingLiftStacking(wings); + // Calculate intra-wing lift stacking by descending down wing hierarchies and calculating the stacking between children of each node. + foreach (var wing in wings) + liftStacking += CalculateIntraWingLiftStacking(wing); + Debug.Log($"DEBUG Lift stacking: {liftStacking}"); + } +#endif } float StatLines = 0; float armorLines = 0; if (!BDArmorySettings.RESET_ARMOUR) { + line += 0.5f; if (Thickness != oldThickness) { oldThickness = Thickness; @@ -305,22 +413,19 @@ void WindowArmor(int windowID) thicknessField["Thickness"].maxValue = maxThickness; CalculateArmorMass(); } - GUI.Label(new Rect(40, line * lineHeight, 300, lineHeight), Localizer.Format("#LOC_BDArmory_ArmorSelect"), style); - line++; + //GUI.Label(new Rect(40, line * lineHeight, 300, lineHeight), StringUtils.Localize("#LOC_BDArmory_ArmorSelect"), style); if (!armorslist) { FillArmorList(); - GUIStyle listStyle = new GUIStyle(BDArmorySetup.BDGuiSkin.button); - listStyle.fixedHeight = 18; //make list contents slightly smaller armorBox = new BDGUIComboBox(new Rect(10, line * lineHeight, 280, lineHeight), new Rect(10, line * lineHeight, 280, lineHeight), armorBoxText, armorGUI, 120, listStyle); armorslist = true; } - + armorBox.UpdateRect(new Rect(10, line * lineHeight, 280, lineHeight)); int selected_index = armorBox.Show(); armorLines++; - if (armorBox.isOpen) + if (armorBox.IsOpen) { - armorLines += 6; + armorLines += armorBox.Height / lineHeight; } if (selected_index != previous_index) { @@ -332,48 +437,49 @@ void WindowArmor(int windowID) CalculateArmorStats(); } previous_index = selected_index; + CalculateArmorMass(); } line += 0.5f; if (GameSettings.ADVANCED_TWEAKABLES) { - ArmorStats = GUI.Toggle(new Rect(10, (line + armorLines) * lineHeight, 280, lineHeight), ArmorStats, Localizer.Format("#LOC_BDArmory_ArmorStats"), ArmorStats ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button); + ArmorStats = GUI.Toggle(new Rect(10, (line + armorLines) * lineHeight, 280, lineHeight), ArmorStats, StringUtils.Localize("#LOC_BDArmory_ArmorStats"), ArmorStats ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button); StatLines++; if (ArmorStats) { - GUI.Label(new Rect(15, (line + armorLines + StatLines) * lineHeight, 120, lineHeight), Localizer.Format("#LOC_BDArmory_ArmorStrength") + " " + ArmorStrength, style); - //StatLines++; - GUI.Label(new Rect(135, (line + armorLines + StatLines) * lineHeight, 260, lineHeight), Localizer.Format("#LOC_BDArmory_ArmorHardness") + " " + ArmorHardness, style); - StatLines++; - GUI.Label(new Rect(15, (line + armorLines + StatLines) * lineHeight, 120, lineHeight), Localizer.Format("#LOC_BDArmory_ArmorDuctility") + " " + ArmorDuctility, style); - //StatLines++; - GUI.Label(new Rect(135, (line + armorLines + StatLines) * lineHeight, 260, lineHeight), Localizer.Format("#LOC_BDArmory_ArmorDiffusivity") + " " + ArmorDiffusivity, style); - StatLines++; - GUI.Label(new Rect(15, (line + armorLines + StatLines) * lineHeight, 120, lineHeight), Localizer.Format("#LOC_BDArmory_ArmorMaxTemp") + " " + ArmorMaxTemp + " K", style); - //StatLines++; - GUI.Label(new Rect(135, (line + armorLines + StatLines) * lineHeight, 260, lineHeight), Localizer.Format("#LOC_BDArmory_ArmorDensity") + " " + ArmorDensity + " kg/m3", style); - StatLines++; - GUI.Label(new Rect(15, (line + armorLines + StatLines) * lineHeight, 120, lineHeight), Localizer.Format("#LOC_BDArmory_ArmorCost") + " " + ArmorCost + "/m3", style); - StatLines++; if (selectedArmor != "None") { - GUI.Label(new Rect(15, (line + armorLines + StatLines) * lineHeight, 260, lineHeight), Localizer.Format("#LOC_BDArmory_BulletResist") + ": " + (relValue < 1.2 ? (relValue < 0.5 ? "* * * * *" : "* * * *") : (relValue > 2.8 ? (relValue > 4 ? "*" : "* *") : "* * *")), style); + GUI.Label(new Rect(15, (line + armorLines + StatLines) * lineHeight, 120, lineHeight), $"{StringUtils.Localize("#LOC_BDArmory_ArmorStrength")}: {ArmorStrength}", style); + //StatLines++; + GUI.Label(new Rect(135, (line + armorLines + StatLines) * lineHeight, 260, lineHeight), $"{StringUtils.Localize("#LOC_BDArmory_ArmorHardness")}: {ArmorHardness} ", style); + StatLines++; + GUI.Label(new Rect(15, (line + armorLines + StatLines) * lineHeight, 120, lineHeight), $"{StringUtils.Localize("#LOC_BDArmory_ArmorDuctility")}: {ArmorDuctility}", style); + //StatLines++; + GUI.Label(new Rect(135, (line + armorLines + StatLines) * lineHeight, 260, lineHeight), $"{StringUtils.Localize("#LOC_BDArmory_ArmorDiffusivity")}: {ArmorDiffusivity}", style); + StatLines++; + GUI.Label(new Rect(15, (line + armorLines + StatLines) * lineHeight, 120, lineHeight), $"{StringUtils.Localize("#LOC_BDArmory_ArmorMaxTemp")}: {ArmorMaxTemp} K", style); + //StatLines++; + GUI.Label(new Rect(135, (line + armorLines + StatLines) * lineHeight, 260, lineHeight), $"{StringUtils.Localize("#LOC_BDArmory_ArmorDensity")}: {ArmorDensity} kg/m3", style); + StatLines++; + GUI.Label(new Rect(15, (line + armorLines + StatLines) * lineHeight, 120, lineHeight), $"{StringUtils.Localize("#LOC_BDArmory_ArmorCost")}: {ArmorCost} /m3", style); + StatLines++; + GUI.Label(new Rect(15, (line + armorLines + StatLines) * lineHeight, 260, lineHeight), $"{StringUtils.Localize("#LOC_BDArmory_BulletResist")}:{(relValue < 1.2 ? (relValue < 0.5 ? "* * * * *" : "* * * *") : (relValue > 2.8 ? (relValue > 4 ? "*" : "* *") : "* * *"))}", style); StatLines++; - GUI.Label(new Rect(15, (line + armorLines + StatLines) * lineHeight, 260, lineHeight), Localizer.Format("#LOC_BDArmory_ExplosionResist") + ": " + ((ArmorDuctility < 0.05f && ArmorHardness < 500) ? "* *" : (exploValue > 8000 ? (exploValue > 20000 ? "* * * * *" : "* * * *") : (exploValue < 4000 ? (exploValue < 2000 ? "*" : "* *") : "* * *"))), style); + GUI.Label(new Rect(15, (line + armorLines + StatLines) * lineHeight, 260, lineHeight), $"{StringUtils.Localize("#LOC_BDArmory_ExplosionResist")}: {((ArmorDuctility < 0.05f && ArmorHardness < 500) ? "* *" : (exploValue > 8000 ? (exploValue > 20000 ? "* * * * *" : "* * * *") : (exploValue < 4000 ? (exploValue < 2000 ? "*" : "* *") : "* * *")))}", style); StatLines++; - GUI.Label(new Rect(15, (line + armorLines + StatLines) * lineHeight, 260, lineHeight), Localizer.Format("#LOC_BDArmory_LaserResist") + ": " + (ArmorDiffusivity > 150 ? (ArmorDiffusivity > 199 ? "* * * * *" : "* * * *") : (ArmorDiffusivity < 50 ? (ArmorDiffusivity < 10 ? "*" : "* *") : "* * *")), style); + GUI.Label(new Rect(15, (line + armorLines + StatLines) * lineHeight, 260, lineHeight), $"{StringUtils.Localize("#LOC_BDArmory_LaserResist")}: {(ArmorDiffusivity > 150 ? (ArmorDiffusivity > 199 ? "* * * * *" : "* * * *") : (ArmorDiffusivity < 50 ? (ArmorDiffusivity < 10 ? "*" : "* *") : "* * *"))}", style); StatLines++; if (ArmorDuctility < 0.05) { - if (ArmorHardness > 500) GUI.Label(new Rect(15, (line + armorLines + StatLines) * lineHeight, 260, lineHeight), Localizer.Format("#LOC_BDArmory_ArmorShatterWarning"), style); + if (ArmorHardness > 500) GUI.Label(new Rect(15, (line + armorLines + StatLines) * lineHeight, 260, lineHeight), StringUtils.Localize("#LOC_BDArmory_ArmorShatterWarning"), style); StatLines++; } } - if (selectedArmor != "Mild Steel" || selectedArmor != "None") + if (selectedArmor != "Mild Steel" && selectedArmor != "None") { - GUI.Label(new Rect(10, (line + armorLines + StatLines) * lineHeight, 300, lineHeight), Localizer.Format("#LOC_BDArmory_EquivalentThickness") + ": " + relValue * Thickness + "mm", style); + GUI.Label(new Rect(10, (line + armorLines + StatLines) * lineHeight, 300, lineHeight), $"{StringUtils.Localize("#LOC_BDArmory_EquivalentThickness")}: {relValue * Thickness} mm", style); line++; } } @@ -383,58 +489,38 @@ void WindowArmor(int windowID) if (!BDArmorySettings.RESET_HULL) { line += 0.5f; - showHullMenu = GUI.Toggle(new Rect(10, (line + armorLines + StatLines) * lineHeight, 280, lineHeight), - showHullMenu, Localizer.Format("#LOC_BDArmory_Armor_HullMat"), showHullMenu ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button); - HullLines += 1.15f; - - if (showHullMenu) + if (!hullslist) { - if (isSteel != (isSteel = GUI.Toggle(new Rect(10, (line + armorLines + StatLines + HullLines) * lineHeight, 280, lineHeight), isSteel, Localizer.Format("#LOC_BDArmory_Steel"), isSteel ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) - { - if (isSteel) - { - isWood = false; - isAluminium = false; - hullmat = 3; - CalculateArmorMass(true); - } - } - HullLines += 1.15f; - if (isWood != (isWood = GUI.Toggle(new Rect(10, (line + armorLines + StatLines + HullLines) * lineHeight, 280, lineHeight), isWood, Localizer.Format("#LOC_BDArmory_Wood"), isWood ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) - { - if (isWood) - { - isAluminium = false; - isSteel = false; - hullmat = 1; - CalculateArmorMass(true); - } - } - HullLines += 1.15f; - if (isAluminium != (isAluminium = GUI.Toggle(new Rect(10, (line + armorLines + StatLines + HullLines) * lineHeight, 280, lineHeight), isAluminium, Localizer.Format("#LOC_BDArmory_Aluminium"), isAluminium ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) - { - if (isAluminium) - { - isWood = false; - isSteel = false; - hullmat = 2; - CalculateArmorMass(true); - } - } - HullLines += 1.15f; - if (!isSteel && !isWood && !isAluminium) + FillHullList(); + hullBox = new BDGUIComboBox(new Rect(10, (line + armorLines + StatLines) * lineHeight, 280, lineHeight), new Rect(10, (line + armorLines + StatLines) * lineHeight, 280, lineHeight), hullBoxText, hullGUI, 120, listStyle); + hullslist = true; + } + hullBox.UpdateRect(new Rect(10, (line + armorLines + StatLines) * lineHeight, 280, lineHeight)); + if (armorLines + StatLines != oldLines) + { + oldLines = armorLines + StatLines; + } + int selected_mat = hullBox.Show(); + HullLines++; + if (hullBox.IsOpen) + { + HullLines += hullBox.Height / lineHeight; + } + if (selected_mat != previous_mat) + { + if (selected_mat != -1) { - isAluminium = true; - hullmat = 2; + hullmat = HullInfo.materials[selected_mat].name; CalculateArmorMass(true); } + previous_mat = selected_mat; } } line += 0.5f; GUI.DragWindow(); height = Mathf.Lerp(height, (line + armorLines + StatLines + HullLines) * lineHeight, 0.15f); windowRect.height = height; - BDGUIUtils.RepositionWindow(ref windowRect); + GUIUtils.RepositionWindow(ref windowRect); } void CalculateArmorMass(bool vesselmass = false) @@ -443,8 +529,6 @@ void CalculateArmorMass(bool vesselmass = false) return; bool modified = false; - totalArmorMass = 0; - totalArmorCost = 0; var selectedArmorIndex = ArmorInfo.armors.FindIndex(t => t.name == selectedArmor); if (selectedArmorIndex < 0) return; @@ -490,12 +574,13 @@ void CalculateArmorMass(bool vesselmass = false) armor.ArmorModified(null, null); modified = true; } - totalArmorMass += armor.armorMass; - totalArmorCost += armor.armorCost; + StartCoroutine(calcArmorMassAndCost()); + //totalArmorMass += armor.armorMass; //these aren't updating due to ArmorModified getting called next Update tick, so armorMass/Cost hasn't updated yet for grabbing the new value + //totalArmorCost += armor.armorCost; } else { - armor.HullTypeNum = hullmat; + armor.HullTypeNum = HullInfo.materials.FindIndex(t => t.name == hullmat) + 1; armor.HullModified(null, null); modified = true; } @@ -516,18 +601,242 @@ void CalculateArmorMass(bool vesselmass = false) ArmorHardness = ArmorInfo.armors[selectedArmorIndex].Hardness; ArmorMaxTemp = ArmorInfo.armors[selectedArmorIndex].SafeUseTemp; ArmorStrength = ArmorInfo.armors[selectedArmorIndex].Strength; + ArmorVfactor = ArmorInfo.armors[selectedArmorIndex].vFactor; + ArmorMu1 = ArmorInfo.armors[selectedArmorIndex].muParam1; + ArmorMu2 = ArmorInfo.armors[selectedArmorIndex].muParam2; + ArmorMu3 = ArmorInfo.armors[selectedArmorIndex].muParam3; if (modified) { shipModifiedfromCalcArmor = true; GameEvents.onEditorShipModified.Fire(EditorLogic.fetch.ship); } + + if (!FerramAerospace.hasFAR) + CalculateTotalLift(); // Re-calculate lift and wing loading on armor change + } + + void CalculateTotalLift() + { + if (EditorLogic.RootPart == null) + return; + + totalLift = 0; + using (List.Enumerator parts = EditorLogic.fetch.ship.Parts.GetEnumerator()) + while (parts.MoveNext()) + { + if (parts.Current.IsMissile()) continue; + ModuleLiftingSurface wing = parts.Current.GetComponent(); + if (wing != null) + { + totalLift += wing.deflectionLiftCoeff * Vector3.Project(wing.transform.forward, Vector3.up).sqrMagnitude; // Only return vertically oriented lift components + } + } + wingLoading = totalLift / EditorLogic.fetch.ship.GetTotalMass(); //convert to kg/m2. 1 LiftingArea is ~ 3.51m2, or ~285kg/m2 + totalLiftArea = totalLift * 3.51f; + WLRatio = (EditorLogic.fetch.ship.GetTotalMass() * 1000) / totalLiftArea; + + CalculateTotalLiftStacking(); + } + + void CalculateTotalLiftStacking() + { + if (EditorLogic.RootPart == null) + return; + + float liftStackedAll = 0; + float liftStackedAllEval = 0; + List evaluatedParts = new List(); ; + totalLiftStackRatio = 0; + using (List.Enumerator parts1 = EditorLogic.fetch.ship.Parts.GetEnumerator()) + while (parts1.MoveNext()) + { + if (parts1.Current.IsMissile()) continue; + if (IsAeroBrake(parts1.Current)) continue; + ModuleLiftingSurface wing1 = parts1.Current.GetComponent(); + if (wing1 != null) + { + evaluatedParts.Add(parts1.Current); + float lift1area = wing1.deflectionLiftCoeff * Vector3.Project(wing1.transform.forward, Vector3.up).sqrMagnitude; // Only return vertically oriented lift components + float lift1rad = BDAMath.Sqrt(lift1area / Mathf.PI); + Vector3 col1Pos = wing1.part.partTransform.TransformPoint(wing1.part.CoLOffset); + Vector3 col1PosProj = col1Pos.ProjectOnPlanePreNormalized(Vector3.up); + liftStackedAllEval += lift1area; // Add up total lift areas + + using (List.Enumerator parts2 = EditorLogic.fetch.ship.Parts.GetEnumerator()) + while (parts2.MoveNext()) + { + if (evaluatedParts.Contains(parts2.Current)) continue; + if (parts1.Current == parts2.Current) continue; + if (parts2.Current.IsMissile()) continue; + if (IsAeroBrake(parts2.Current)) continue; + ModuleLiftingSurface wing2 = parts2.Current.GetComponent(); + if (wing2 != null) + { + float lift2area = wing2.deflectionLiftCoeff * Vector3.Project(wing2.transform.forward, Vector3.up).sqrMagnitude; // Only return vertically oriented lift components + float lift2rad = BDAMath.Sqrt(lift2area / Mathf.PI); + Vector3 col2Pos = wing2.part.partTransform.TransformPoint(wing2.part.CoLOffset); + Vector3 col2PosProj = col2Pos.ProjectOnPlanePreNormalized(Vector3.up); + + float d = Vector3.Distance(col1PosProj, col2PosProj); + float R = lift1rad; + float r = lift2rad; + + float a = 0; + + // Calc overlapping area between two circles + if (d >= R + r) // Circles not overlapping + a = 0; + else if (R >= (d + r)) // Circle 2 inside Circle 1 + a = Mathf.PI * r * r; + else if (r >= (d + R)) // Circle 1 inside Circle 2 + a = Mathf.PI * R * R; + else if (d < R + r) // Circles overlapping + a = r * r * Mathf.Acos((d * d + r * r - R * R) / (2 * d * r)) + R * R * Mathf.Acos((d * d + R * R - r * r) / (2 * d * R)) - + 0.5f * BDAMath.Sqrt((-d + r + R) * (d + r - R) * (d - r + R) * (d + r + R)); + + // Calculate vertical spacing factor (0 penalty if surfaces are spaced sqrt(2*lift) apart) + float v_dist = Vector3.Distance(Vector3.Project(col1Pos, Vector3.up), Vector3.Project(col2Pos, Vector3.up)); + float l_spacing = Mathf.Round(Mathf.Max(lift1area, lift2area, 0.25f) * 100f) / 100f; // Round lift to nearest 0.01 + float v_factor = Mathf.Pow(Mathf.Clamp01((BDAMath.Sqrt(2 * l_spacing) - v_dist) / (BDAMath.Sqrt(2 * l_spacing) - BDAMath.Sqrt(l_spacing))), 0.1f); + + // Add overlapping area + liftStackedAll += a * v_factor; + } + } + } + } + // Look at total overlapping lift area as a percentage of total lift area. Since overlapping lift area for multiple parts can potentially be greater than the total lift area, cap + // the stacking at 100%. Also, multiply stacked lift by two for the edge case where only two parts are evaluated. + liftStackedAll *= (evaluatedParts.Count == 2) ? 2 : 1; + totalLiftStackRatio = Mathf.Clamp01(liftStackedAll / Mathf.Max(liftStackedAllEval, 0.01f)); + } + + /// + /// Get a list of all the logical wings (hierarchically connected) on a vessel beginning at (but not including) the given part. + /// + /// The part to start at or the root part if not specified. + /// + /// + /// A list of the logical wings where each wing is a hashset of parts with lifting surfaces. + List> FindWings(Part part = null, HashSet checkedParts = null, List> wings = null) + { + if (part == null) part = EditorLogic.RootPart; + if (wings == null) wings = new List>(); + if (part == null) return wings; + if (checkedParts == null) checkedParts = new HashSet { part }; + + foreach (var child in part.children) + { + if (child == null) continue; + if (child.IsMissile()) continue; + if (!checkedParts.Contains(child)) // If the part hasn't been checked, check it for being the start of a wing. + { + var liftingSurface = child.GetComponent(); + if (liftingSurface != null) // Start of a wing. + { + var wing = FindWingDescendants(child); + wings.Add(wing); + checkedParts.UnionWith(wing); // Mark all the wing segments as being checked already. + } + } + checkedParts.Add(child); + FindWings(child, checkedParts, wings); // We still need to check all the children in case there's another wing lower in the hierarchy. + } + + return wings; + } + + /// + /// Find connected wing segments that are direct descendants of a part. + /// + /// + /// The parts that form the segments of the wing. + HashSet FindWingDescendants(Part wing) + { + HashSet segments = new HashSet { wing }; + foreach (var child in wing.children) + { + if (child == null) continue; + if (child.IsMissile()) continue; + var liftingSurface = child.GetComponent(); + if (liftingSurface != null) // If the child is a lifting surface, add it and its descendants. + { + segments.Add(child); + segments.UnionWith(FindWingDescendants(child)); + } + } + return segments; + } + + /// + /// Calculate the amount of lift stacking between the wings. + /// + /// The wings, each consisting of a hashset of parts. + /// The base part of the wing (leave as null if the base isn't a wing). + /// The amount of stacking between the wings. + float CalculateInterWingLiftStacking(List> wings, Part baseWing = null) + { + if (wings.Count < (baseWing == null ? 2 : 1)) return 0; // Not enough segments for an overlap. + var wingRoots = wings.Select(wing => wing.Where(p => p.parent == null || !wing.Contains(p.parent)).FirstOrDefault()).Where(p => p != null).ToList(); + Debug.Log($"DEBUG Checking lift stacking between wings with{(baseWing != null ? $" base {baseWing.name}:{baseWing.persistentId} and" : "")} roots: {string.Join(", ", wingRoots.Select(w => $"{w.name}:{w.persistentId}"))}"); + return 0; // FIXME Compute the lift of the base and each wing and the amount they overlap. This could potentially include non-vertical lift too. + } + + /// + /// Calculate the amount of lift stacking between segments of a wing. + /// + /// The parts in the wing. + /// The amount of stacking within the wing. + float CalculateIntraWingLiftStacking(HashSet wing) + { + var wingRoot = wing.Where(p => p.parent == null || !wing.Contains(p.parent)).FirstOrDefault(); // The root of the wing either has no parent or the parent isn't part of the wing. + if (wingRoot == null) return 0; + var subWings = FindWings(wingRoot); + float liftStacking = CalculateInterWingLiftStacking(subWings, wingRoot); // Include the lift stacking between this wing segment and its sub-wings. + foreach (var subWing in subWings) liftStacking += CalculateIntraWingLiftStacking(subWing); // Then go deeper in the tree. + return liftStacking; + } + + bool IsAeroBrake(Part part) + { + if (part.GetComponent() is not null) + { + if (part.GetComponent() is not null) + return true; + else + return false; + } + else + return false; + } + + IEnumerator calcArmorMassAndCost() + { + yield return new WaitForEndOfFrame(); + yield return new WaitForEndOfFrame(); + totalArmorMass = 0; + totalArmorCost = 0; + using (List.Enumerator parts = EditorLogic.fetch.ship.Parts.GetEnumerator()) + while (parts.MoveNext()) + { + if (parts.Current.IsMissile()) continue; + HitpointTracker armor = parts.Current.GetComponent(); + if (armor != null) + { + if (armor.ArmorTypeNum == 1 && !armor.ArmorPanel) continue; + + totalArmorMass += armor.armorMass; + totalArmorCost += armor.armorCost; + } + } } + void Visualize() { if (EditorLogic.RootPart == null) return; - if (Visualizer || HPvisualizer || HullVisualizer) + if (Visualizer || HPvisualizer || HullVisualizer || LiftVisualizer) { using (List.Enumerator parts = EditorLogic.fetch.ship.Parts.GetEnumerator()) while (parts.MoveNext()) @@ -543,7 +852,15 @@ void Visualize() } if (HullVisualizer) { - VisualizerColor = Color.HSVToRGB(a.HullTypeNum / 3, 1, 1f); + VisualizerColor = Color.HSVToRGB(a.HullTypeNum / (HullInfo.materials.Count + 1), 1, 1f); + } + if (LiftVisualizer) + { + ModuleLiftingSurface wing = parts.Current.GetComponent(); + if (wing != null && wing.deflectionLiftCoeff > 0f) + VisualizerColor = Color.HSVToRGB(Mathf.Clamp01(Mathf.Log10(wing.deflectionLiftCoeff + 1f)) / 3, 1, 1); + else + VisualizerColor = Color.HSVToRGB(0, 0, 0.5f); } var r = parts.Current.GetComponentsInChildren(); { @@ -578,7 +895,7 @@ void Visualize() } } } - if (!Visualizer && !HPvisualizer && !HullVisualizer) + if (!Visualizer && !HPvisualizer && !HullVisualizer && !LiftVisualizer) { using (List.Enumerator parts = EditorLogic.fetch.ship.Parts.GetEnumerator()) while (parts.MoveNext()) @@ -656,6 +973,7 @@ void Visualize() oldVisualizer = Visualizer; oldHPvisualizer = HPvisualizer; oldHullVisualizer = HullVisualizer; + oldLiftVisualizer = LiftVisualizer; refreshVisualizer = false; refreshHPvisualizer = false; refreshHullvisualizer = false; @@ -705,15 +1023,17 @@ private void CalculateArmorStats() } else { - float bulletEnergy = ProjectileUtils.CalculateProjectileEnergy(0.388f, 1109); + /*float bulletEnergy = ProjectileUtils.CalculateProjectileEnergy(0.388f, 1109); var modifiedCaliber = (15) + (15) * (2f * ArmorDuctility * ArmorDuctility); float yieldStrength = modifiedCaliber * modifiedCaliber * Mathf.PI / 100f * ArmorStrength * (ArmorDensity / 7850f) * 30; if (ArmorDuctility > 0.25f) { yieldStrength *= 0.7f; } - float newCaliber = ProjectileUtils.CalculateDeformation(yieldStrength, bulletEnergy, 30, 1109, ArmorHardness, ArmorDensity, 0.19f, 0.8f); - armorValue = ProjectileUtils.CalculatePenetration(30, newCaliber, 0.388f, 1109, ArmorDuctility, ArmorDensity, ArmorStrength, 30, 0.8f, false); + float newCaliber = ProjectileUtils.CalculateDeformation(yieldStrength, bulletEnergy, 30, 1109, 1176, 7850, 0.19f, 0.8f, false); + */ + //armorValue = ProjectileUtils.CalculatePenetration(30, newCaliber, 0.388f, 1109, ArmorDuctility, ArmorDensity, ArmorStrength, 30, 0.8f, false); + armorValue = ProjectileUtils.CalculatePenetration(30, 1109, 0.388f, 0.8f, ArmorStrength, ArmorVfactor, ArmorMu1, ArmorMu2, ArmorMu3); //why is this hardcoded? it needs to be the selected armor mat's vars relValue = Mathf.Round(armorValue / steelValue * 10) / 10; exploValue = ArmorStrength * (1 + ArmorDuctility) * (ArmorDensity / 1000); } diff --git a/BDArmory/UI/BDAEditorCategory.cs b/BDArmory/UI/BDAEditorCategory.cs index 0a3dc4167..f33f17b92 100644 --- a/BDArmory/UI/BDAEditorCategory.cs +++ b/BDArmory/UI/BDAEditorCategory.cs @@ -1,13 +1,19 @@ using System.Collections; using System.Collections.Generic; -using BDArmory.Control; -using BDArmory.Core; -using BDArmory.CounterMeasure; -using BDArmory.Modules; using KSP.UI; using KSP.UI.Screens; using UnityEngine; +using BDArmory.Control; +using BDArmory.CounterMeasure; +using BDArmory.Radar; +using BDArmory.Settings; +using BDArmory.Targeting; +using BDArmory.Utils; +using BDArmory.Weapons; +using BDArmory.Weapons.Missiles; +using BDArmory.WeaponMounts; + namespace BDArmory.UI { [KSPAddon(KSPAddon.Startup.EditorAny, false)] @@ -87,7 +93,7 @@ private void Awake() { if (parts.Current == null || !parts.Current.partPrefab || parts.Current.partConfig == null) continue; - if (parts.Current.partConfig.HasValue(BDACategoryKey) || parts.Current.manufacturer == Misc.BDAEditorTools.Manufacturer) + if (parts.Current.partConfig.HasValue(BDACategoryKey) || parts.Current.manufacturer == BDAEditorTools.Manufacturer) { partsDetected = true; GameEvents.onGUIEditorToolbarReady.Add(LoadBDArmoryCategory); @@ -101,6 +107,15 @@ private void Awake() { if (parts.Current.partConfig == null || parts.Current.partPrefab == null) continue; + if (parts.Current.partConfig.HasValue("TechRequired")) + { + var research = parts.Current.partConfig.GetValue("Techrequired"); + if (research == "Unresearchable") + { + parts.Current.partConfig.RemoveValue(BDACategoryKey); + continue; + } + } if (parts.Current.partConfig.HasValue(BDACategoryKey)) parts.Current.partConfig.AddValue(AutoBDACategoryKey, parts.Current.partConfig.GetValue(BDACategoryKey)); else @@ -254,8 +269,8 @@ private void DrawSettingsWindow(int id) PartCategorizer.Instance.editorPartList.Refresh(); } - BDGUIUtils.RepositionWindow(ref SettingsWindow); - BDGUIUtils.UseMouseEventInRect(SettingsWindow); + GUIUtils.RepositionWindow(ref SettingsWindow); + GUIUtils.UseMouseEventInRect(SettingsWindow); } private void CreateBDAPartBar() @@ -278,7 +293,7 @@ private void CreateBDAPartBar() foundCategories.Add(cat); } // If part does not have a bdacategory but manufacturer is BDA. - else if (parts.Current.manufacturer == Misc.BDAEditorTools.Manufacturer) + else if (parts.Current.manufacturer == BDAEditorTools.Manufacturer) foundLegacy = true; } Categories.RemoveAll(s => !foundCategories.Contains(s) && s != "All"); @@ -349,7 +364,7 @@ private bool PartInCurrentCategory(AvailablePart part) return part.partConfig.HasValue(BDArmorySettings.AUTOCATEGORIZE_PARTS ? AutoBDACategoryKey : BDACategoryKey); case "Legacy": - return part.manufacturer == Misc.BDAEditorTools.Manufacturer; + return part.manufacturer == BDAEditorTools.Manufacturer; case "Misc": { diff --git a/BDArmory/UI/BDATargetManager.cs b/BDArmory/UI/BDATargetManager.cs index c429e3850..27074aa26 100644 --- a/BDArmory/UI/BDATargetManager.cs +++ b/BDArmory/UI/BDATargetManager.cs @@ -1,19 +1,22 @@ using System; using System.IO; +using System.Linq; using System.Collections; using System.Collections.Generic; using System.Text; -using BDArmory.Core; -using BDArmory.Core.Extension; -using BDArmory.Core.Utils; +using UnityEngine; + +using BDArmory.Bullets; +using BDArmory.Competition; +using BDArmory.Control; using BDArmory.CounterMeasure; -using BDArmory.Misc; -using BDArmory.Modules; -using BDArmory.Parts; +using BDArmory.Extensions; using BDArmory.Radar; +using BDArmory.Settings; using BDArmory.Targeting; -using BDArmory.Bullets; -using UnityEngine; +using BDArmory.Utils; +using BDArmory.Weapons; +using BDArmory.Weapons.Missiles; namespace BDArmory.UI { @@ -29,8 +32,10 @@ public class BDATargetManager : MonoBehaviour public static List LoadedBuildings; public static List LoadedVessels; public static BDATargetManager Instance; + static List hottestPart = new List(); private StringBuilder debugString = new StringBuilder(); + private int debugStringLineCount = 0; private float updateTimer = 0; static string gpsTargetsCfg; @@ -67,6 +72,7 @@ void OnDestroy() GameEvents.onVesselGoOffRails.Remove(AddVessel); GameEvents.onVesselCreate.Remove(AddVessel); GameEvents.onVesselDestroy.Remove(CleanVesselList); + DestructibleBuilding.OnLoaded.Remove(AddBuilding); } void Start() @@ -135,15 +141,21 @@ void CleanVesselList(Vessel v) void Update() { - if (BDArmorySettings.DRAW_DEBUG_LABELS && FlightGlobals.ready) + if (!FlightGlobals.ready) return; + + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) { - updateTimer -= Time.fixedDeltaTime; + updateTimer -= Time.deltaTime; if (updateTimer < 0) { UpdateDebugLabels(); - updateTimer = 0.5f; //next update in half a sec only + updateTimer = 1f; //next update in one sec only } } + else + { + if (debugString.Length > 0) debugString.Clear(); + } } public static void RegisterLaserPoint(ModuleTargetingCamera cam) @@ -211,7 +223,7 @@ private static ModuleTargetingCamera GetModuleTargeting(bool parentOnly, Vector3 public static bool CanSeePosition(Vector3 groundTargetPosition, Vector3 vesselPosition, Vector3 missilePosition) { - if ((groundTargetPosition - vesselPosition).sqrMagnitude < Mathf.Pow(20, 2)) + if ((groundTargetPosition - vesselPosition).sqrMagnitude < 400) // 20 * 20 { return false; } @@ -220,7 +232,7 @@ public static bool CanSeePosition(Vector3 groundTargetPosition, Vector3 vesselPo Ray ray = new Ray(missilePosition, groundTargetPosition - missilePosition); ray.origin += 10 * ray.direction; RaycastHit rayHit; - if (Physics.Raycast(ray, out rayHit, dist, (int)(LayerMasks.Parts | LayerMasks.Scenery | LayerMasks.Unknown19))) + if (Physics.Raycast(ray, out rayHit, dist, (int)(LayerMasks.Parts | LayerMasks.Scenery | LayerMasks.Unknown19 | LayerMasks.Wheels))) { if ((rayHit.point - groundTargetPosition).sqrMagnitude < 200) { @@ -241,18 +253,144 @@ public static bool CanSeePosition(Vector3 groundTargetPosition, Vector3 vesselPo /// /// Vessel /// Heat signature value - public static float GetVesselHeatSignature(Vessel v) + public static Tuple GetVesselHeatSignature(Vessel v, Vector3 sensorPosition = default(Vector3), float frontAspectModifier = 1f, FloatCurve tempSensitivity = default(FloatCurve)) { float heatScore = 0f; - + float minHeat = float.MaxValue; + Part IRPart = null; + float occludedPlumeHeatScore = 0; + hottestPart.Clear(); using (List.Enumerator part = v.Parts.GetEnumerator()) while (part.MoveNext()) { if (!part.Current) continue; float thisScore = (float)(part.Current.thermalInternalFluxPrevious + part.Current.skinTemperature); + thisScore *= (tempSensitivity != default(FloatCurve)) ? tempSensitivity.Evaluate(thisScore) : 1f; heatScore = Mathf.Max(heatScore, thisScore); + minHeat = Mathf.Min(minHeat, thisScore); + if (thisScore == heatScore) IRPart = part.Current; + } + if (sensorPosition != default(Vector3)) //Heat source found; now lets determine how much of the craft is occluding it + { + using (List.Enumerator part = v.Parts.GetEnumerator()) + while (part.MoveNext()) + { + if (!part.Current) continue; + float thisScore = (float)(part.Current.thermalInternalFluxPrevious + part.Current.skinTemperature); + thisScore *= (tempSensitivity != default(FloatCurve)) ? tempSensitivity.Evaluate(thisScore) : 1f; + if (thisScore < heatScore * 1.05f && thisScore > heatScore * 0.95f) + { + hottestPart.Add(part.Current); + } + } + Part closestPart = null; + Transform thrustTransform = null; + bool afterburner = false; + bool propEngine = false; + float distance = 9999999; + if (hottestPart.Count > 0) + { + RaycastHit[] hits = new RaycastHit[10]; + using (List.Enumerator part = hottestPart.GetEnumerator()) //might be multiple 'hottest' parts (multi-engine ship, etc), find the one closest to the sensor + { + while (part.MoveNext()) + { + if (!part.Current) continue; + float thisdistance = Vector3.Distance(part.Current.transform.position, sensorPosition); + if (distance > thisdistance) + { + distance = thisdistance; + closestPart = part.Current; + } + } + IRPart = closestPart; + if (BDArmorySettings.DEBUG_RADAR) Debug.Log("[IRSTdebugging] closest heatsource found: " + closestPart.name + ", heat: " + (float)(closestPart.thermalInternalFluxPrevious + closestPart.skinTemperature)); + } + if (closestPart != null) + { + TargetInfo tInfo; + if (tInfo = v.gameObject.GetComponent()) + { + if (tInfo.targetEngineList.Contains(closestPart)) + { + string transformName = (closestPart.GetComponent()) ? closestPart.GetComponent().thrustVectorTransformName : "thrustTransform"; + thrustTransform = closestPart.FindModelTransform(transformName); + propEngine = (closestPart.GetComponent()) ? closestPart.GetComponent().velCurve.Evaluate(1.1f) <= 0 : false; // Props don't generate thrust above Mach 1--will catch props that don't use Firespitter + if (!propEngine) + afterburner = (closestPart.GetComponent()) ? !(closestPart.GetComponent().runningPrimary) : false; + } + } + // Set thrustTransform as heat source position for engines + Vector3 heatSourcePosition = propEngine ? closestPart.transform.position : thrustTransform ? thrustTransform.position : closestPart.transform.position; + Ray partRay = new Ray(heatSourcePosition, sensorPosition - heatSourcePosition); //trace from heatsource to IR sensor + + // First evaluate occluded heat score, then if the closestPart is a non-prop engine, evaluate the plume temperature + float occludedPartHeatScore = GetOccludedHeatScore(v, closestPart, heatSourcePosition, heatScore, partRay, hits, distance, thrustTransform, false, propEngine, frontAspectModifier); + if (thrustTransform && !propEngine) + { + // For plume, evaluate at 3m behind engine thrustTransform at 72% engine heat (based on DC-9 plume measurements) + if (afterburner) heatSourcePosition = thrustTransform.position + thrustTransform.forward.normalized * 3f; + partRay = new Ray(heatSourcePosition, sensorPosition - heatSourcePosition); //trace from heatsource to IR sensor + occludedPlumeHeatScore = GetOccludedHeatScore(v, closestPart, heatSourcePosition, 0.72f * heatScore, partRay, hits, distance, thrustTransform, true, propEngine, frontAspectModifier); + heatScore = Mathf.Max(occludedPartHeatScore, occludedPlumeHeatScore); // + } + else + { + heatScore = occludedPartHeatScore; + } + } + } + } + heatScore = Mathf.Max(heatScore, minHeat * frontAspectModifier); // Don't allow occluded heat to be below lowest temperature part on craft (while incorporating frontAspectModifier) + VesselCloakInfo vesselcamo = v.gameObject.GetComponent(); + if (vesselcamo && vesselcamo.cloakEnabled) + { + heatScore *= vesselcamo.thermalReductionFactor; + heatScore = Mathf.Max(heatScore, occludedPlumeHeatScore); //Fancy heatsinks/thermoptic camo isn't going to magically cool the engine plume + } + if (BDArmorySettings.DEBUG_RADAR) Debug.Log("[IRSTdebugging] final heatScore: " + heatScore); + return new Tuple(heatScore, IRPart); + } + + static float GetOccludedHeatScore(Vessel v, Part closestPart, Vector3 heatSourcePosition, float heatScore, Ray partRay, RaycastHit[] hits, float distance, Transform thrustTransform = null, bool enginePlume = false, bool propEngine = false, float frontAspectModifier = 1f) + { + var layerMask = (int)(LayerMasks.Parts | LayerMasks.EVA | LayerMasks.Wheels); + + var hitCount = Physics.RaycastNonAlloc(partRay, hits, distance, layerMask); + if (hitCount == hits.Length) + { + hits = Physics.RaycastAll(partRay, distance, layerMask); + hitCount = hits.Length; + } + float OcclusionFactor = 0; + float SpacingConstant = 64; + float lastHeatscore = 0; + int DebugCount = 0; + using (var hitsEnu = hits.Take(hitCount).OrderBy(x => x.distance).GetEnumerator()) + while (hitsEnu.MoveNext()) + { + Part partHit = hitsEnu.Current.collider.GetComponentInParent(); + if (partHit == null) continue; + if (ProjectileUtils.IsIgnoredPart(partHit)) continue; // Ignore ignored parts. + if (partHit == closestPart) continue; //ignore the heatsource + if (partHit.vessel != v) continue; //ignore irstCraft; does also mean that in edge case of one craft occluded behind a second craft from PoV of a third craft w/irst wouldn't actually occlude, but oh well + //The heavier/further the part, the more it's going to occlude the heatsource + DebugCount++; + float sqrSpacing = (heatSourcePosition - partHit.transform.position).sqrMagnitude; + OcclusionFactor += partHit.mass * (1 - Mathf.Clamp01(sqrSpacing / SpacingConstant)); // occlusions from heavy parts close to the heatsource matter most + lastHeatscore = (float)(partHit.thermalInternalFluxPrevious + partHit.skinTemperature); } + // Factor in occlusion from engines if they are the heat source, ignoring engine self-occlusion for prop engines or within ~50 deg cone of engine exhaust + if (thrustTransform && !propEngine && (Vector3.Dot(thrustTransform.transform.forward, partRay.direction.normalized) < 0.65f)) + { + DebugCount++; + float sqrSpacing = (heatSourcePosition - thrustTransform.position).sqrMagnitude; + OcclusionFactor += closestPart.mass * (1 - Mathf.Clamp01(sqrSpacing / SpacingConstant)); + } + if (BDArmorySettings.DEBUG_RADAR) Debug.Log("[IRSTdebugging] occlusion found: " + (1 + OcclusionFactor) + "; " + DebugCount + " occluding parts"); + if (OcclusionFactor > 0) heatScore = Mathf.Max(lastHeatscore, heatScore / (1 + OcclusionFactor)); + if ((OcclusionFactor > 0) || enginePlume || propEngine) heatScore *= frontAspectModifier; // Apply front aspect modifier when heat is being evaluated outside ~50 deg cone of engine exhaust return heatScore; } @@ -260,7 +398,7 @@ public static float GetVesselHeatSignature(Vessel v) /// /// Find a flare closest in heat signature to passed heat signature /// - public static TargetSignatureData GetFlareTarget(Ray ray, float scanRadius, float highpassThreshold, bool allAspect, FloatCurve lockedSensorFOVBias, FloatCurve lockedSensorVelocityBias, TargetSignatureData heatTarget) + public static TargetSignatureData GetFlareTarget(Ray ray, float scanRadius, float highpassThreshold, FloatCurve lockedSensorFOVBias, FloatCurve lockedSensorVelocityBias, TargetSignatureData heatTarget) { TargetSignatureData flareTarget = TargetSignatureData.noTarget; float heatSignature = heatTarget.signalStrength; @@ -304,13 +442,13 @@ public static TargetSignatureData GetFlareTarget(Ray ray, float scanRadius, floa return flareTarget; } - public static TargetSignatureData GetHeatTarget(Vessel sourceVessel, Vessel missileVessel, Ray ray, TargetSignatureData priorHeatTarget, float scanRadius, float highpassThreshold, bool allAspect, FloatCurve lockedSensorFOVBias, FloatCurve lockedSensorVelocityBias, MissileFire mf = null, bool favorGroundTargets = false) + public static TargetSignatureData GetHeatTarget(Vessel sourceVessel, Vessel missileVessel, Ray ray, TargetSignatureData priorHeatTarget, float scanRadius, float highpassThreshold, float frontAspectHeatModifier, bool uncagedLock, FloatCurve lockedSensorFOVBias, FloatCurve lockedSensorVelocityBias, MissileFire mf = null, TargetInfo desiredTarget = null) { float minMass = 0.05f; //otherwise the RAMs have trouble shooting down incoming missiles TargetSignatureData finalData = TargetSignatureData.noTarget; float finalScore = 0; float priorHeatScore = priorHeatTarget.signalStrength; - + Tuple IRSig; foreach (Vessel vessel in LoadedVessels) { if (vessel == null) @@ -319,26 +457,34 @@ public static TargetSignatureData GetHeatTarget(Vessel sourceVessel, Vessel miss continue; if (vessel == sourceVessel || vessel == missileVessel) continue; - if (favorGroundTargets && !vessel.LandedOrSplashed) + if (vessel.vesselType == VesselType.Debris) + continue; + if (mf != null && mf.guardMode && (desiredTarget == null || desiredTarget.Vessel != vessel)) //clamp heaters to desired target { - // for AGM heat guidance + //Debug.Log($"[BDATargetManager] looking at {vessel.GetName()}; has MF: {mf}; Guardmode: {(mf != null ? mf.guardMode.ToString() : "N/A")}"); continue; } TargetInfo tInfo = vessel.gameObject.GetComponent(); if (tInfo == null) - return finalData; + { + var WM = VesselModuleRegistry.GetMissileFire(vessel, true); + if (WM != null) + { + tInfo = vessel.gameObject.AddComponent(); + } + else + return finalData; //This is causing Heaters to not work under manual control - Need Guardmode to generate TargetInfos. Could either add missing TI here, or have UpdateGuardScan proc all the time, not just in guardmode + } // If no weaponManager or no target or the target is not a missile with engines on..??? and the target weighs less than 50kg, abort. if (mf == null || !tInfo || !(mf && tInfo.isMissile && (tInfo.MissileBaseModule.MissileState == MissileBase.MissileStates.Boost || tInfo.MissileBaseModule.MissileState == MissileBase.MissileStates.Cruise))) { if (vessel.GetTotalMass() < minMass) - { continue; - } } // Abort if target is friendly. @@ -357,30 +503,25 @@ public static TargetSignatureData GetHeatTarget(Vessel sourceVessel, Vessel miss float angle = Vector3.Angle(vessel.CoM - ray.origin, ray.direction); - if ((angle < scanRadius) || (allAspect && !priorHeatTarget.exists)) // Allow allAspect=true missiles to find target outside of seeker FOV before launch + if ((angle < scanRadius) || (uncagedLock && !priorHeatTarget.exists)) // Allow allAspect=true missiles to find target outside of seeker FOV before launch { if (RadarUtils.TerrainCheck(ray.origin, vessel.transform.position)) continue; - if (!allAspect) + if (!uncagedLock) { - if (!Utils.CheckSightLineExactDistance(ray.origin, vessel.CoM + vessel.Velocity(), Vector3.Distance(vessel.CoM, ray.origin), 5, 5)) + if (!OtherUtils.CheckSightLineExactDistance(ray.origin, vessel.CoM + vessel.Velocity(), Vector3.Distance(vessel.CoM, ray.origin), 5, 5)) continue; } - float score = GetVesselHeatSignature(vessel) * Mathf.Clamp01(15 / angle); - score *= (1400 * 1400) / Mathf.Clamp((vessel.CoM - ray.origin).sqrMagnitude, 90000, 36000000); + IRSig = GetVesselHeatSignature(vessel, BDArmorySettings.ASPECTED_IR_SEEKERS ? missileVessel.CoM : Vector3.zero, frontAspectHeatModifier); //change vector3.zero to missile.transform.position to have missile IR detection dependant on target aspect + float score = IRSig.Item1 * Mathf.Clamp01(15 / angle); + score *= (1400 * 1400) / Mathf.Max((vessel.CoM - ray.origin).sqrMagnitude, 90000); // Clamp below 300m // Add bias targets closer to center of seeker FOV, only once missile seeker can see target if ((priorHeatScore > 0f) && (angle < scanRadius)) - score *= GetSeekerBias(angle, Vector3.Angle(vessel.Velocity(), priorHeatTarget.velocity), lockedSensorFOVBias, lockedSensorVelocityBias); - if (vessel.LandedOrSplashed && !favorGroundTargets) - { - score /= 4; - } - score *= Mathf.Clamp(Vector3.Angle(vessel.transform.position - ray.origin, -VectorUtils.GetUpDirection(ray.origin)) / 90, 0.5f, 1.5f); if ((finalScore > 0f) && (score > 0f) && (priorHeatScore > 0)) // If we were passed a target heat score, look for the most similar non-zero heat score after picking a target @@ -388,7 +529,7 @@ public static TargetSignatureData GetHeatTarget(Vessel sourceVessel, Vessel miss if (Mathf.Abs(score - priorHeatScore) < Mathf.Abs(finalScore - priorHeatScore)) { finalScore = score; - finalData = new TargetSignatureData(vessel, score); + finalData = new TargetSignatureData(vessel, score, IRSig.Item2); } } else // Otherwise, pick the highest heat score @@ -396,19 +537,19 @@ public static TargetSignatureData GetHeatTarget(Vessel sourceVessel, Vessel miss if (score > finalScore) { finalScore = score; - finalData = new TargetSignatureData(vessel, score); + finalData = new TargetSignatureData(vessel, score, IRSig.Item2); } } + //Debug.Log($"[IR DEBUG] heatscore of {vessel.GetName()} is {score}"); } } - // see if there are flares decoying us: bool flareSuccess = false; TargetSignatureData flareData = TargetSignatureData.noTarget; if (priorHeatScore > 0) // Flares can only decoy if we already had a target { - flareData = GetFlareTarget(ray, scanRadius, highpassThreshold, allAspect, lockedSensorFOVBias, lockedSensorVelocityBias, priorHeatTarget); + flareData = GetFlareTarget(ray, scanRadius, highpassThreshold, lockedSensorFOVBias, lockedSensorVelocityBias, priorHeatTarget); flareSuccess = ((!flareData.Equals(TargetSignatureData.noTarget)) && (flareData.signalStrength > highpassThreshold)); } @@ -427,7 +568,7 @@ public static TargetSignatureData GetHeatTarget(Vessel sourceVessel, Vessel miss // See if a flare is closer in score to priorHeatScore than finalScore if (priorHeatScore > 0) flareSuccess = (Mathf.Abs(flareData.signalStrength - priorHeatScore) < Mathf.Abs(finalScore - priorHeatScore)) && flareSuccess; - else if (BDArmorySettings.DUMB_IR_SEEKERS) + else if (BDArmorySettings.DUMB_IR_SEEKERS) //convert to a missile .cfg option for earlier-gen IR missiles? flareSuccess = (flareData.signalStrength > finalScore) && flareSuccess; else flareSuccess = false; @@ -450,15 +591,19 @@ private static float GetSeekerBias(float anglePos, float angleVel, FloatCurve se void UpdateDebugLabels() { debugString.Length = 0; + debugStringLineCount = 0; using (var team = TargetDatabase.GetEnumerator()) while (team.MoveNext()) { + if (!LoadedVesselSwitcher.Instance.WeaponManagers.Any(wm => wm.Key == team.Current.Key.Name)) continue; debugString.AppendLine($"Team {team.Current.Key} targets:"); + ++debugStringLineCount; foreach (TargetInfo targetInfo in team.Current.Value) { if (targetInfo) { + if (!targetInfo.isMissile && targetInfo.weaponManager == null) continue; if (!targetInfo.Vessel) { debugString.AppendLine($"- A target with no vessel reference."); @@ -472,24 +617,35 @@ void UpdateDebugLabels() { debugString.AppendLine($"- null target info."); } + ++debugStringLineCount; } } debugString.Append(Environment.NewLine); - debugString.AppendLine($"Heat Signature: {GetVesselHeatSignature(FlightGlobals.ActiveVessel):#####}"); - debugString.AppendLine($"Radar Signature: " + RadarUtils.GetVesselRadarSignature(FlightGlobals.ActiveVessel).radarModifiedSignature); - debugString.AppendLine($"Chaff multiplier: " + RadarUtils.GetVesselChaffFactor(FlightGlobals.ActiveVessel)); + debugString.AppendLine($"Base Heat Signature: {GetVesselHeatSignature(FlightGlobals.ActiveVessel, Vector3.zero):#####}, For/Aft: " + + GetVesselHeatSignature(FlightGlobals.ActiveVessel, FlightGlobals.ActiveVessel.vesselTransform.position + 100f * FlightGlobals.ActiveVessel.vesselTransform.up).Item1.ToString("0") + "/" + + GetVesselHeatSignature(FlightGlobals.ActiveVessel, FlightGlobals.ActiveVessel.vesselTransform.position - 100f * FlightGlobals.ActiveVessel.vesselTransform.up).Item1.ToString("0") + ", Side: " + + GetVesselHeatSignature(FlightGlobals.ActiveVessel, FlightGlobals.ActiveVessel.vesselTransform.position + 100f * FlightGlobals.ActiveVessel.vesselTransform.right).Item1.ToString("0") + ", Top/Bot: " + + GetVesselHeatSignature(FlightGlobals.ActiveVessel, FlightGlobals.ActiveVessel.vesselTransform.position - 100f * FlightGlobals.ActiveVessel.vesselTransform.forward).Item1.ToString("0") + "/" + + GetVesselHeatSignature(FlightGlobals.ActiveVessel, FlightGlobals.ActiveVessel.vesselTransform.position + 100f * FlightGlobals.ActiveVessel.vesselTransform.forward).Item1.ToString("0")); + var radarSig = RadarUtils.GetVesselRadarSignature(FlightGlobals.ActiveVessel); + debugString.AppendLine($"Radar Signature: " + radarSig.radarModifiedSignature); + debugString.AppendLine($"Chaff multiplier: " + RadarUtils.GetVesselChaffFactor(FlightGlobals.ActiveVessel).ToString("0.0")); var ecmjInfo = FlightGlobals.ActiveVessel.gameObject.GetComponent(); + var cloakInfo = FlightGlobals.ActiveVessel.gameObject.GetComponent(); debugString.AppendLine($"ECM Jammer Strength: " + (ecmjInfo != null ? ecmjInfo.jammerStrength.ToString("0.00") : "N/A")); debugString.AppendLine($"ECM Lockbreak Strength: " + (ecmjInfo != null ? ecmjInfo.lockBreakStrength.ToString("0.00") : "N/A")); - debugString.AppendLine($"Radar Lockbreak Factor: " + RadarUtils.GetVesselRadarSignature(FlightGlobals.ActiveVessel).radarLockbreakFactor); + debugString.AppendLine($"Radar Lockbreak Factor: " + radarSig.radarLockbreakFactor.ToString("0.0")); + debugString.AppendLine("Visibility Modifiers: " + (cloakInfo != null ? $"Optical: {(cloakInfo.opticalReductionFactor * 100).ToString("0.00")}%, " + + $"Thermal: {(cloakInfo.thermalReductionFactor * 100).ToString("0.00")}%" : "N/A")); + debugStringLineCount += 8; } public void SaveGPSTargets(ConfigNode saveNode = null) { string saveTitle = HighLogic.CurrentGame.Title; - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BDATargetManager]: Save title: " + saveTitle); + if (BDArmorySettings.DEBUG_RADAR) Debug.Log("[BDArmory.BDATargetManager]: Save title: " + saveTitle); ConfigNode fileNode = ConfigNode.Load(gpsTargetsCfg); if (fileNode == null) { @@ -547,7 +703,7 @@ public void SaveGPSTargets(ConfigNode saveNode = null) string targetString = GPSListToString(); gpsNode.SetValue("Targets", targetString, true); fileNode.Save(gpsTargetsCfg); - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BDATargetManager]: ==== Saved BDA GPS Targets ===="); + if (BDArmorySettings.DEBUG_RADAR) Debug.Log("[BDArmory.BDATargetManager]: ==== Saved BDA GPS Targets ===="); } } @@ -635,14 +791,14 @@ public List Load() //format: very mangled json :( private string GPSListToString() { - return Utils.JsonCompat(JsonUtility.ToJson(new SerializableGPSData(GPSTargets))); + return OtherUtils.JsonCompat(JsonUtility.ToJson(new SerializableGPSData(GPSTargets))); } private void StringToGPSList(string listString) { try { - GPSTargets = JsonUtility.FromJson(Utils.JsonDecompat(listString)).Load(); + GPSTargets = JsonUtility.FromJson(OtherUtils.JsonDecompat(listString)).Load(); Debug.Log("[BDArmory.BDATargetManager]: Loaded GPS Targets."); } @@ -680,7 +836,7 @@ public static void RemoveTarget(TargetInfo target) db.Current.Value.Remove(target); } - public static void ReportVessel(Vessel v, MissileFire reporter) + public static void ReportVessel(Vessel v, MissileFire reporter, bool radar = false) { if (!v) return; if (!reporter) return; @@ -696,6 +852,10 @@ public static void ReportVessel(Vessel v, MissileFire reporter) { info = v.gameObject.AddComponent(); info.detectedTime[reporter.Team] = Time.time; + if (radar) + { + info.detected[reporter.Team] = true; + } break; } } @@ -710,6 +870,10 @@ public static void ReportVessel(Vessel v, MissileFire reporter) { info = v.gameObject.AddComponent(); info.detectedTime[reporter.Team] = Time.time; + if (radar) + { + info.detected[reporter.Team] = true; + } break; } } @@ -720,7 +884,24 @@ public static void ReportVessel(Vessel v, MissileFire reporter) if (info && reporter.Team.IsEnemy(info.Team)) { AddTarget(info, reporter.Team); - info.detectedTime[reporter.Team] = Time.time; + info.detectedTime[reporter.Team] = Time.time; //time since last detected + if (radar) + { + info.detected[reporter.Team] = true; //target is under radar detection + } + } + } + + public static void ClearRadarReport(Vessel v, MissileFire reporter) + { + if (!v) return; + if (!reporter) return; + + TargetInfo info = v.gameObject.GetComponent(); + + if (info && reporter.Team.IsEnemy(info.Team)) + { + info.detected[reporter.Team] = false; } } @@ -744,6 +925,7 @@ public static List TargetList(BDTeam team) public static void ClearDatabase() { + if (TargetDatabase is null) return; TargetDatabase.Clear(); } @@ -758,6 +940,9 @@ public static TargetInfo GetAirToAirTarget(MissileFire mf) { if (target.Current == null) continue; if (target.Current.NumFriendliesEngaging(mf.Team) >= 2) continue; + if (target.Current.weaponManager == null) continue; + if ((mf.multiTargetNum > 1 || mf.multiMissileTgtNum > 1) && mf.targetsAssigned.Contains(target.Current)) continue; + //if (mf.vessel.GetName().Contains(BDArmorySettings.REMOTE_ORCHESTRATION_NPC_SWAPPER) && target.Current.Vessel.GetName().Contains(BDArmorySettings.REMOTE_ORCHESTRATION_NPC_SWAPPER)) continue; if (target.Current && target.Current.Vessel && target.Current.isFlying && !target.Current.isMissile && target.Current.isThreat) { Vector3 targetRelPos = target.Current.Vessel.vesselTransform.position - mf.vessel.vesselTransform.position; @@ -786,6 +971,7 @@ public static TargetInfo GetAirToAirTargetAbortExtend(MissileFire mf, float maxD while (target.MoveNext()) { if (target.Current == null || !target.Current.Vessel || target.Current.isLandedOrSurfaceSplashed || target.Current.isMissile || !target.Current.isThreat) continue; + if (target.Current.weaponManager == null) continue; Vector3 targetRelPos = target.Current.Vessel.vesselTransform.position - mf.vessel.vesselTransform.position; float distance, dot; @@ -815,6 +1001,7 @@ public static TargetInfo GetClosestFriendly(MissileFire mf) while (target.MoveNext()) { if (target.Current == null || !target.Current.Vessel || target.Current.weaponManager == mf) continue; + if (target.Current.weaponManager == null) continue; if (finalTarget == null || (target.Current.IsCloser(finalTarget, mf))) { finalTarget = target.Current; @@ -830,6 +1017,7 @@ public static TargetInfo GetTargetFromWeaponManager(MissileFire mf) while (target.MoveNext()) { if (target.Current == null) continue; + if (target.Current.weaponManager == null) continue; if (target.Current.Vessel && target.Current.weaponManager == mf) { return target.Current; @@ -846,7 +1034,8 @@ public static TargetInfo GetClosestTarget(MissileFire mf) while (target.MoveNext()) { if (target.Current == null) continue; - if (mf.multiTargetNum > 1 && mf.targetsAssigned.Contains(target.Current)) continue; + if (target.Current.weaponManager == null) continue; + if ((mf.multiTargetNum > 1 || mf.multiMissileTgtNum > 1) && mf.targetsAssigned.Contains(target.Current)) continue; if (target.Current && target.Current.Vessel && mf.CanSeeTarget(target.Current) && !target.Current.isMissile) { if (finalTarget == null || (target.Current.IsCloser(finalTarget, mf))) @@ -866,7 +1055,8 @@ public static List GetAllTargetsExcluding(List excluding while (target.MoveNext()) { if (target.Current == null) continue; - if (mf.multiTargetNum > 1 && mf.targetsAssigned.Contains(target.Current)) continue; + if (target.Current.weaponManager == null) continue; + //if ((mf.multiTargetNum > 1 || mf.multiMissileTgtNum > 1) && mf.targetsAssigned.Contains(target.Current)) continue; if (target.Current && target.Current.Vessel && mf.CanSeeTarget(target.Current) && !excluding.Contains(target.Current)) { finalTargets.Add(target.Current); @@ -883,6 +1073,8 @@ public static TargetInfo GetLeastEngagedTarget(MissileFire mf) while (target.MoveNext()) { if (target.Current == null || target.Current.Vessel == null) continue; + if (target.Current.weaponManager == null) continue; + if ((mf.multiTargetNum > 1 || mf.multiMissileTgtNum > 1) && mf.targetsAssigned.Contains(target.Current)) continue; if (mf.CanSeeTarget(target.Current) && !target.Current.isMissile && target.Current.isThreat) { if (finalTarget == null || target.Current.NumFriendliesEngaging(mf.Team) < finalTarget.NumFriendliesEngaging(mf.Team)) @@ -905,12 +1097,14 @@ public static TargetInfo GetClosestTargetWithBiasAndHysteresis(MissileFire mf) while (target.MoveNext()) { if (target.Current == null || target.Current.Vessel == null) continue; - if (mf.multiTargetNum > 1 && mf.targetsAssigned.Contains(target.Current)) continue; + if (target.Current.weaponManager == null) continue; + if ((mf.multiTargetNum > 1 || mf.multiMissileTgtNum > 1) && mf.targetsAssigned.Contains(target.Current)) continue; if (mf.CanSeeTarget(target.Current) && !target.Current.isMissile && target.Current.isThreat) { float theta = Vector3.Angle(mf.vessel.srf_vel_direction, target.Current.transform.position - mf.vessel.transform.position); float distance = (mf.vessel.transform.position - target.Current.position).magnitude; - float targetScore = (target.Current == mf.currentTarget ? hysteresis : 1f) * ((bias - 1f) * Mathf.Pow(Mathf.Cos(theta / 2f), 2f) + 1f) / distance; + float cosTheta2 = Mathf.Cos(theta / 2f); + float targetScore = (target.Current == mf.currentTarget ? hysteresis : 1f) * ((bias - 1f) * cosTheta2 * cosTheta2 + 1f) / distance; if (finalTarget == null || targetScore > finalTargetScore) { finalTarget = target.Current; @@ -929,17 +1123,22 @@ public static TargetInfo GetHighestPriorityTarget(MissileFire mf) using (var target = TargetList(mf.Team).GetEnumerator()) while (target.MoveNext()) { - if (mf.multiTargetNum > 1 && mf.targetsAssigned.Contains(target.Current)) continue; + if (target.Current == null) continue; + if (target.Current.weaponManager == null) continue; + //Debug.Log("[BDArmory.BDATargetmanager]: evaluating " + target.Current.Vessel.GetName()); + if ((mf.multiTargetNum > 1 || mf.multiMissileTgtNum > 1) && mf.targetsAssigned.Contains(target.Current)) continue; if (target.Current != null && target.Current.Vessel && mf.CanSeeTarget(target.Current) && !target.Current.isMissile && target.Current.isThreat) { float targetScore = (target.Current == mf.currentTarget ? mf.targetBias : 1f) * ( 1f + mf.targetWeightRange * target.Current.TargetPriRange(mf) + + mf.targetWeightAirPreference * target.Current.TargetPriEngagement(target.Current.weaponManager) + mf.targetWeightATA * target.Current.TargetPriATA(mf) + mf.targetWeightAccel * target.Current.TargetPriAcceleration() + mf.targetWeightClosureTime * target.Current.TargetPriClosureTime(mf) + mf.targetWeightWeaponNumber * target.Current.TargetPriWeapons(target.Current.weaponManager, mf) + mf.targetWeightMass * target.Current.TargetPriMass(target.Current.weaponManager, mf) + + mf.targetWeightDamage * target.Current.TargetPriDmg(target.Current.weaponManager) + mf.targetWeightFriendliesEngaging * target.Current.TargetPriFriendliesEngaging(mf) + mf.targetWeightThreat * target.Current.TargetPriThreat(target.Current.weaponManager, mf) + mf.targetWeightAoD * target.Current.TargetPriAoD(mf) + @@ -953,7 +1152,7 @@ public static TargetInfo GetHighestPriorityTarget(MissileFire mf) } } } - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_AI) Debug.Log("[BDArmory.BDATargetManager]: Selected " + (finalTarget != null ? finalTarget.Vessel.GetDisplayName() : "null") + " with target score of " + finalTargetScore.ToString("0.00")); mf.UpdateTargetPriorityUI(finalTarget); @@ -969,7 +1168,7 @@ public static TargetInfo GetMissileTarget(MissileFire mf, bool targetingMeOnly = while (target.MoveNext()) { if (target.Current == null) continue; - if (mf.multiTargetNum > 1 && mf.targetsAssigned.Contains(target.Current)) continue; + if ((mf.multiTargetNum > 1 || mf.multiMissileTgtNum > 1) && mf.targetsAssigned.Contains(target.Current)) continue; if (target.Current && target.Current.Vessel && target.Current.isMissile && target.Current.isThreat && mf.CanSeeTarget(target.Current)) { if (target.Current.MissileBaseModule) @@ -991,7 +1190,7 @@ public static TargetInfo GetMissileTarget(MissileFire mf, bool targetingMeOnly = } else { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_MISSILES) Debug.LogWarning("[BDArmory.BDATargetManager]: checking target missile - doesn't have missile module"); } @@ -1010,7 +1209,7 @@ public static TargetInfo GetUnengagedMissileTarget(MissileFire mf) while (target.MoveNext()) { if (target.Current == null) continue; - if (mf.multiTargetNum > 1 && mf.targetsAssigned.Contains(target.Current)) continue; + if ((mf.multiTargetNum > 1 || mf.multiMissileTgtNum > 1) && mf.targetsAssigned.Contains(target.Current)) continue; if (target.Current && target.Current.Vessel && mf.CanSeeTarget(target.Current) && target.Current.isMissile && RadarUtils.MissileIsThreat(target.Current.MissileBaseModule, mf, false)) { if (target.Current.NumFriendliesEngaging(mf.Team) == 0) @@ -1030,7 +1229,7 @@ public static TargetInfo GetClosestMissileTarget(MissileFire mf) while (target.MoveNext()) { if (target.Current == null) continue; - if (mf.multiTargetNum > 1 && mf.targetsAssigned.Contains(target.Current)) continue; + if ((mf.multiTargetNum > 1 || mf.multiMissileTgtNum > 1) && mf.targetsAssigned.Contains(target.Current)) continue; if (target.Current && target.Current.Vessel && mf.CanSeeTarget(target.Current) && target.Current.isMissile) { bool isHostile = false; @@ -1048,7 +1247,7 @@ public static TargetInfo GetClosestMissileTarget(MissileFire mf) return finalTarget; } - //checks to see if a friendly is too close to the gun trajectory to fire them + //checks to see if a friendly is too close to the gun trajectory to fire them // Replaced by ModuleWeapon.CheckForFriendlies() public static bool CheckSafeToFireGuns(MissileFire weaponManager, Vector3 aimDirection, float safeDistance, float cosUnsafeAngle) { if (weaponManager == null) return false; @@ -1075,9 +1274,9 @@ public static bool CheckSafeToFireGuns(MissileFire weaponManager, Vector3 aimDir void OnGUI() { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_AI) { - GUI.Label(new Rect(600, 100, 600, 600), debugString.ToString()); + GUI.Label(new Rect(600, 100, 600, 16 * debugStringLineCount), debugString.ToString()); } } } diff --git a/BDArmory/UI/BDATeamIcons.cs b/BDArmory/UI/BDATeamIcons.cs index d3ccb2197..957fa186b 100644 --- a/BDArmory/UI/BDATeamIcons.cs +++ b/BDArmory/UI/BDATeamIcons.cs @@ -1,9 +1,12 @@ using System.Collections.Generic; -using BDArmory.Modules; +using System; using UnityEngine; + using BDArmory.Competition; -using BDArmory.Competition.VesselSpawning; -using System; +using BDArmory.Settings; +using BDArmory.Utils; +using BDArmory.VesselSpawning; +using BDArmory.Weapons.Missiles; namespace BDArmory.UI { @@ -24,7 +27,7 @@ void Awake() Instance = this; } GUIStyle IconUIStyle; - + GUIStyle DropshadowStyle; GUIStyle mIStyle; Color Teamcolor; @@ -35,6 +38,11 @@ private void Start() IconUIStyle.fontSize = 10; IconUIStyle.normal.textColor = XKCDColors.Red;//replace with BDATISetup defined value varable. + DropshadowStyle = new GUIStyle(); + DropshadowStyle.fontStyle = FontStyle.Bold; + DropshadowStyle.fontSize = 10; + DropshadowStyle.normal.textColor = Color.black; + mIStyle = new GUIStyle(); mIStyle.fontStyle = FontStyle.Normal; mIStyle.fontSize = 10; @@ -48,7 +56,7 @@ private void DrawOnScreenIcon(Vector3 worldPos, Texture texture, Vector2 size, C if (Event.current.type.Equals(EventType.Repaint)) { bool offscreen = false; - Vector3 screenPos = BDGUIUtils.GetMainCamera().WorldToViewportPoint(worldPos); + Vector3 screenPos = GUIUtils.GetMainCamera().WorldToViewportPoint(worldPos); if (screenPos.z < 0) { offscreen = true; @@ -104,8 +112,8 @@ private void DrawThreatIndicator(Vector3 vesselPos, Vector3 targetPos, Color Tea { if (Event.current.type.Equals(EventType.Repaint)) { - Vector3 screenPos = BDGUIUtils.GetMainCamera().WorldToViewportPoint(vesselPos); - Vector3 screenTPos = BDGUIUtils.GetMainCamera().WorldToViewportPoint(targetPos); + Vector3 screenPos = GUIUtils.GetMainCamera().WorldToViewportPoint(vesselPos); + Vector3 screenTPos = GUIUtils.GetMainCamera().WorldToViewportPoint(targetPos); if (screenTPos.z > 0) { float xPos = (screenPos.x * Screen.width); @@ -168,7 +176,7 @@ public Vector2 calculateRadialCoords(Vector2 RadialCoord, Vector2 Tail, float an } public static void DrawPointer(Vector2 Pointer, float angle, float width, Color color) { - Camera cam = BDGUIUtils.GetMainCamera(); + Camera cam = GUIUtils.GetMainCamera(); if (cam == null) return; @@ -177,7 +185,7 @@ public static void DrawPointer(Vector2 Pointer, float angle, float width, Color Rect upRect = new Rect(Pointer.x - (width / 2), Pointer.y - length, width, length); GUIUtility.RotateAroundPivot(-angle + 180, Pointer); - BDGUIUtils.DrawRectangle(upRect, color); + GUIUtils.DrawRectangle(upRect, color); GUI.matrix = Matrix4x4.identity; } void OnGUI() @@ -190,17 +198,22 @@ void OnGUI() using (List.Enumerator v = FlightGlobals.Vessels.GetEnumerator()) while (v.MoveNext()) { - if (v.Current == null) continue; - if (!v.Current.loaded || v.Current.packed || v.Current.isActiveVessel) continue; - if (VesselModuleRegistry.ignoredVesselTypes.Contains(v.Current.vesselType)) continue; - + if (v.Current == null || v.Current.packed || !v.Current.loaded) continue; if (BDTISettings.MISSILES) { using (var ml = VesselModuleRegistry.GetModules(v.Current).GetEnumerator()) while (ml.MoveNext()) { if (ml.Current == null) continue; - if (ml.Current.MissileState != MissileBase.MissileStates.Idle && ml.Current.MissileState != MissileBase.MissileStates.Drop) + MissileLauncher launcher = ml.Current as MissileLauncher; + //if (ml.Current.MissileState != MissileBase.MissileStates.Idle && ml.Current.MissileState != MissileBase.MissileStates.Drop) + + bool multilauncher = false; + if (launcher != null) + { + if (launcher.multiLauncher && !launcher.multiLauncher.isClusterMissile) multilauncher = true; + } + if ((ml.Current.HasFired && !multilauncher) && !ml.Current.HasMissed && !ml.Current.HasExploded) //culling post-thrust missiles makes AGMs get cleared almost immediately after launch { Vector3 sPos = FlightGlobals.ActiveVessel.vesselTransform.position; Vector3 tPos = v.Current.vesselTransform.position; @@ -220,29 +233,41 @@ void OnGUI() UoM = "m"; UIdist = Dist.magnitude.ToString("0.0"); } - BDGUIUtils.DrawTextureOnWorldPos(v.Current.CoM, BDTISetup.Instance.TextureIconMissile, new Vector2(20, 20), 0); - if (BDGUIUtils.WorldToGUIPos(ml.Current.vessel.CoM, out guiPos)) + GUIUtils.DrawTextureOnWorldPos(v.Current.CoM, BDTISetup.Instance.TextureIconMissile, new Vector2(20, 20), 0); + if (GUIUtils.WorldToGUIPos(ml.Current.vessel.CoM, out guiPos)) { Rect distRect = new Rect((guiPos.x - 12), (guiPos.y + 10), 100, 32); GUI.Label(distRect, UIdist + UoM, mIStyle); } - + if (BDTISettings.VESSELNAMES) + { + if (GUIUtils.WorldToGUIPos(ml.Current.vessel.CoM, out guiPos)) + { + IconUIStyle.normal.textColor = BDTISetup.Instance.ColorAssignments.ContainsKey(ml.Current.Team.Name) ? BDTISetup.Instance.ColorAssignments[ml.Current.Team.Name] : Color.gray; + Rect nameRect = new Rect((guiPos.x + (24 * BDTISettings.ICONSCALE)), guiPos.y - 4, 100, 32); + Rect shadowRect = new Rect((nameRect.x + 1), nameRect.y + 1, 100, 32); + GUI.Label(shadowRect, ml.Current.vessel.vesselName, DropshadowStyle); + GUI.Label(nameRect, ml.Current.vessel.vesselName, IconUIStyle); + } + } } } } } + + if (!v.Current.loaded || v.Current.packed || v.Current.isActiveVessel) continue; if (BDTISettings.DEBRIS) { - if (v.Current.vesselType != VesselType.Debris && !v.Current.isActiveVessel) continue; + if (v.Current == null) continue; + if (v.Current.vesselType != VesselType.Debris) continue; if (v.Current.LandedOrSplashed) continue; + + Vector3 sPos = FlightGlobals.ActiveVessel.vesselTransform.position; + Vector3 tPos = v.Current.vesselTransform.position; + Vector3 Dist = (tPos - sPos); + if (Dist.magnitude > BDTISettings.DISTANCE_THRESHOLD) { - Vector3 sPos = FlightGlobals.ActiveVessel.vesselTransform.position; - Vector3 tPos = v.Current.vesselTransform.position; - Vector3 Dist = (tPos - sPos); - if (Dist.magnitude > BDTISettings.DISTANCE_THRESHOLD) - { - BDGUIUtils.DrawTextureOnWorldPos(v.Current.CoM, BDTISetup.Instance.TextureIconDebris, new Vector2(20, 20), 0); - } + GUIUtils.DrawTextureOnWorldPos(v.Current.CoM, BDTISetup.Instance.TextureIconDebris, new Vector2(20, 20), 0); } } } @@ -277,9 +302,11 @@ void OnGUI() if (BDTISettings.VESSELNAMES) { Vector2 guiPos; - if (BDGUIUtils.WorldToGUIPos(wm.Current.vessel.CoM, out guiPos)) + if (GUIUtils.WorldToGUIPos(wm.Current.vessel.CoM, out guiPos)) { Rect nameRect = new Rect((guiPos.x + (24 * BDTISettings.ICONSCALE)), guiPos.y - 4, 100, 32); + Rect shadowRect = new Rect((nameRect.x + 1), nameRect.y + 1, 100, 32); + GUI.Label(shadowRect, wm.Current.vessel.vesselName, DropshadowStyle); GUI.Label(nameRect, wm.Current.vessel.vesselName, IconUIStyle); } } @@ -323,17 +350,21 @@ void OnGUI() } } } - if (BDGUIUtils.WorldToGUIPos(wm.Current.vessel.CoM, out guiPos)) + if (GUIUtils.WorldToGUIPos(wm.Current.vessel.CoM, out guiPos)) { if (BDTISettings.VESSELNAMES) { vName = wm.Current.vessel.vesselName; Rect nameRect = new Rect((guiPos.x + (24 * BDTISettings.ICONSCALE)), guiPos.y - 4, 100, 32); + Rect shadowRect = new Rect((nameRect.x + 1), nameRect.y + 1, 100, 32); + GUI.Label(shadowRect, vName, DropshadowStyle); GUI.Label(nameRect, vName, IconUIStyle); } if (BDTISettings.TEAMNAMES) { Rect teamRect = new Rect((guiPos.x + (16 * BDTISettings.ICONSCALE)), (guiPos.y - (19 * BDTISettings.ICONSCALE)), 100, 32); + Rect shadowRect = new Rect((teamRect.x + 1), teamRect.y + 1, 100, 32); + GUI.Label(shadowRect, "Team: " + $"{wm.Current.Team.Name}", DropshadowStyle); GUI.Label(teamRect, "Team: " + $"{wm.Current.Team.Name}", IconUIStyle); } @@ -356,6 +387,8 @@ void OnGUI() } Rect scoreRect = new Rect((guiPos.x + (16 * BDTISettings.ICONSCALE)), (guiPos.y + (14 * BDTISettings.ICONSCALE)), 100, 32); + Rect shadowRect = new Rect((scoreRect.x + 1), scoreRect.y + 1, 100, 32); + GUI.Label(shadowRect, "Score: " + Score, DropshadowStyle); GUI.Label(scoreRect, "Score: " + Score, IconUIStyle); } if (BDTISettings.HEALTHBAR) @@ -368,16 +401,20 @@ void OnGUI() Rect barRect = new Rect((guiPos.x - (32 * BDTISettings.ICONSCALE)), (guiPos.y + (30 * BDTISettings.ICONSCALE)), (64 * BDTISettings.ICONSCALE), 12); Rect healthRect = new Rect((guiPos.x - (30 * BDTISettings.ICONSCALE)), (guiPos.y + (32 * BDTISettings.ICONSCALE)), (60 * (float)hpPercent * BDTISettings.ICONSCALE), 8); //GUI.Label(healthRect, "Team: " + $"{wm.Current.Team.Name}", IconUIStyle); - BDGUIUtils.DrawRectangle(barRect, XKCDColors.Grey); - BDGUIUtils.DrawRectangle(healthRect, Color.HSVToRGB((85f * (float)hpPercent) / 255, 1f, 1f)); + GUIUtils.DrawRectangle(barRect, XKCDColors.Grey); + GUIUtils.DrawRectangle(healthRect, Color.HSVToRGB((85f * (float)hpPercent) / 255, 1f, 1f)); } Rect distRect = new Rect((guiPos.x - 12), (guiPos.y + (45 * BDTISettings.ICONSCALE)), 100, 32); + Rect shadowRect = new Rect((distRect.x + 1), distRect.y + 1, 100, 32); + GUI.Label(shadowRect, UIdist + UoM, DropshadowStyle); GUI.Label(distRect, UIdist + UoM, IconUIStyle); } else { Rect distRect = new Rect((guiPos.x - 12), (guiPos.y + (20 * BDTISettings.ICONSCALE)), 100, 32); + Rect shadowRect = new Rect((distRect.x + 1), distRect.y + 1, 100, 32); + GUI.Label(shadowRect, UIdist + UoM, DropshadowStyle); GUI.Label(distRect, UIdist + UoM, IconUIStyle); } if (BDTISettings.TELEMETRY) @@ -389,19 +426,31 @@ void OnGUI() AIstate = "Pilot " + wm.Current.AI.currentStatus; } Rect telemetryRect = new Rect((guiPos.x + (32 * BDTISettings.ICONSCALE)), guiPos.y + 32, 200, 32); + Rect shadowRect = new Rect((telemetryRect.x + 1), telemetryRect.y + 1, 100, 32); + GUI.Label(shadowRect, selectedWeapon, DropshadowStyle); GUI.Label(telemetryRect, selectedWeapon, IconUIStyle); Rect telemetryRect2 = new Rect((guiPos.x + (32 * BDTISettings.ICONSCALE)), guiPos.y + 48, 200, 32); + Rect shadowRect2 = new Rect((telemetryRect2.x + 1), telemetryRect2.y + 1, 100, 32); + GUI.Label(telemetryRect2, AIstate, DropshadowStyle); GUI.Label(telemetryRect2, AIstate, IconUIStyle); if (wm.Current.isFlaring || wm.Current.isChaffing || wm.Current.isECMJamming) { Rect telemetryRect3 = new Rect((guiPos.x + (32 * BDTISettings.ICONSCALE)), guiPos.y + 64, 200, 32); + Rect shadowRect3 = new Rect((telemetryRect3.x + 1), telemetryRect3.y + 1, 100, 32); + GUI.Label(shadowRect3, "Deploying Counter-Measures", DropshadowStyle); GUI.Label(telemetryRect3, "Deploying Counter-Measures", IconUIStyle); } Rect SpeedRect = new Rect((guiPos.x - (96 * BDTISettings.ICONSCALE)), guiPos.y + 64, 100, 32); + Rect shadowRect4 = new Rect((SpeedRect.x + 1), SpeedRect.y + 1, 100, 32); + GUI.Label(shadowRect4, "Speed: " + wm.Current.vessel.speed.ToString("0.0") + "m/s", DropshadowStyle); GUI.Label(SpeedRect, "Speed: " + wm.Current.vessel.speed.ToString("0.0") + "m/s", IconUIStyle); Rect RAltRect = new Rect((guiPos.x - (96 * BDTISettings.ICONSCALE)), guiPos.y + 80, 100, 32); + Rect shadowRect5 = new Rect((RAltRect.x + 1), RAltRect.y + 1, 100, 32); + GUI.Label(shadowRect5, "Alt: " + wm.Current.vessel.altitude.ToString("0.0") + "m", DropshadowStyle); GUI.Label(RAltRect, "Alt: " + wm.Current.vessel.altitude.ToString("0.0") + "m", IconUIStyle); Rect ThrottleRect = new Rect((guiPos.x - (96 * BDTISettings.ICONSCALE)), guiPos.y + 96, 100, 32); + Rect shadowRect6 = new Rect((ThrottleRect.x + 1), ThrottleRect.y + 1, 100, 32); + GUI.Label(shadowRect6, "Throttle: " + Mathf.CeilToInt(wm.Current.vessel.ctrlState.mainThrottle * 100) + "%", DropshadowStyle); GUI.Label(ThrottleRect, "Throttle: " + Mathf.CeilToInt(wm.Current.vessel.ctrlState.mainThrottle * 100) + "%", IconUIStyle); } } diff --git a/BDArmory/Misc/BDATooltips.cs b/BDArmory/UI/BDATooltips.cs similarity index 88% rename from BDArmory/Misc/BDATooltips.cs rename to BDArmory/UI/BDATooltips.cs index df535da30..ce29f2a74 100644 --- a/BDArmory/Misc/BDATooltips.cs +++ b/BDArmory/UI/BDATooltips.cs @@ -1,6 +1,6 @@ -using BDArmory.UI; +using BDArmory.Settings; -namespace BDArmory.Misc +namespace BDArmory.UI { public static class BDATooltips { diff --git a/BDArmory/UI/BDAmmoSelector.cs b/BDArmory/UI/BDAmmoSelector.cs index 697885ee6..c11c67673 100644 --- a/BDArmory/UI/BDAmmoSelector.cs +++ b/BDArmory/UI/BDAmmoSelector.cs @@ -1,13 +1,12 @@ -using System.Collections; -using BDArmory.Core; -using BDArmory.Misc; -using BDArmory.Modules; -using UnityEngine; using KSP.Localization; using System.Collections.Generic; +using System; +using UnityEngine; using static UnityEngine.GUILayout; + using BDArmory.Bullets; -using System; +using BDArmory.Utils; +using BDArmory.Weapons; namespace BDArmory.UI { @@ -41,7 +40,7 @@ public class BDAmmoSelector : MonoBehaviour public List BList = new List(); public List ammoDesc = new List(); private BulletInfo bulletInfo; - public string guiAmmoTypeString = Localizer.Format("#LOC_BDArmory_Ammo_Slug"); + public string guiAmmoTypeString = StringUtils.Localize("#LOC_BDArmory_Ammo_Slug"); GUIStyle labelStyle; GUIStyle titleStyle; @@ -69,7 +68,7 @@ public void Open(ModuleWeapon weapon, Vector2 position) GUIstring = String.Empty; countString = String.Empty; lastGUIstring = String.Empty; - roundCounter = 1; + roundCounter = 0; if (weapon.ammoBelt != "def") { beltString = weapon.ammoBelt; @@ -80,8 +79,8 @@ public void Open(ModuleWeapon weapon, Vector2 position) if (BList[i] != lastGUIstring) { GUIstring += countString.ToString(); - GUIstring += binfo.DisplayName; - lastGUIstring = binfo.DisplayName; + GUIstring += (string.IsNullOrEmpty(binfo.DisplayName) ? binfo.name : binfo.DisplayName); + lastGUIstring = (string.IsNullOrEmpty(binfo.DisplayName) ? binfo.name : binfo.DisplayName); roundCounter = 1; countString = "; "; } @@ -100,43 +99,47 @@ public void Open(ModuleWeapon weapon, Vector2 position) guiAmmoTypeString = ""; if (bulletInfo.subProjectileCount >= 2) { - guiAmmoTypeString = Localizer.Format("#LOC_BDArmory_Ammo_Shot") + " "; + guiAmmoTypeString = StringUtils.Localize("#LOC_BDArmory_Ammo_Shot") + " "; } if (bulletInfo.apBulletMod >= 1.1) { - guiAmmoTypeString += Localizer.Format("#LOC_BDArmory_Ammo_AP") + " "; + guiAmmoTypeString += StringUtils.Localize("#LOC_BDArmory_Ammo_AP") + " "; } if (bulletInfo.apBulletMod < 1.1 && bulletInfo.apBulletMod > 0.8f) { - guiAmmoTypeString += Localizer.Format("#LOC_BDArmory_Ammo_SAP") + " "; + guiAmmoTypeString += StringUtils.Localize("#LOC_BDArmory_Ammo_SAP") + " "; } if (bulletInfo.nuclear) { - guiAmmoTypeString += Localizer.Format("#LOC_BDArmory_Ammo_Nuclear") + " "; + guiAmmoTypeString += StringUtils.Localize("#LOC_BDArmory_Ammo_Nuclear") + " "; } - if (bulletInfo.explosive && !bulletInfo.nuclear) + if (bulletInfo.tntMass > 0 && !bulletInfo.nuclear) { if (bulletInfo.fuzeType.ToLower() == "flak" || bulletInfo.fuzeType.ToLower() == "proximity") { - guiAmmoTypeString += Localizer.Format("#LOC_BDArmory_Ammo_Flak") + " "; + guiAmmoTypeString += StringUtils.Localize("#LOC_BDArmory_Ammo_Flak") + " "; + } + else if (bulletInfo.explosive.ToLower() == "Shaped") + { + guiAmmoTypeString += StringUtils.Localize("#LOC_BDArmory_Ammo_Shaped") + " "; } - guiAmmoTypeString += Localizer.Format("#LOC_BDArmory_Ammo_Explosive") + " "; + guiAmmoTypeString += StringUtils.Localize("#LOC_BDArmory_Ammo_Explosive") + " "; } if (bulletInfo.incendiary) { - guiAmmoTypeString += Localizer.Format("#LOC_BDArmory_Ammo_Incendiary") + " "; + guiAmmoTypeString += StringUtils.Localize("#LOC_BDArmory_Ammo_Incendiary") + " "; } if (bulletInfo.EMP && !bulletInfo.nuclear) { - guiAmmoTypeString += Localizer.Format("#LOC_BDArmory_Ammo_EMP") + " "; + guiAmmoTypeString += StringUtils.Localize("#LOC_BDArmory_Ammo_EMP") + " "; } if (bulletInfo.beehive) { - guiAmmoTypeString += Localizer.Format("#LOC_BDArmory_Ammo_Beehive") + " "; + guiAmmoTypeString += StringUtils.Localize("#LOC_BDArmory_Ammo_Beehive") + " "; } - if (!bulletInfo.explosive && bulletInfo.apBulletMod <= 0.8) + if (bulletInfo.tntMass <= 0 && bulletInfo.apBulletMod <= 0.8) { - guiAmmoTypeString += Localizer.Format("#LOC_BDArmory_Ammo_Slug"); + guiAmmoTypeString += StringUtils.Localize("#LOC_BDArmory_Ammo_Slug"); } ammoDesc.Add(guiAmmoTypeString); } @@ -172,7 +175,7 @@ protected virtual void OnGUI() } if (open) { - windowRect = GUI.Window(this.GetInstanceID(), windowRect, AmmoSelectorWindow, "", BDArmorySetup.BDGuiSkin.window); + windowRect = GUI.Window(GUIUtility.GetControlID(FocusType.Passive), windowRect, AmmoSelectorWindow, "", BDArmorySetup.BDGuiSkin.window); } PreventClickThrough(); } @@ -180,7 +183,7 @@ private void AmmoSelectorWindow(int id) { float line = 0.5f; string labelString = GUIstring.ToString() + countString.ToString(); - GUI.Label(new Rect(margin, 0.5f * buttonHeight, width - 2 * margin, buttonHeight), Localizer.Format("#LOC_BDArmory_Ammo_Setup"), titleStyle); + GUI.Label(new Rect(margin, 0.5f * buttonHeight, width - 2 * margin, buttonHeight), StringUtils.Localize("#LOC_BDArmory_Ammo_Setup"), titleStyle); if (GUI.Button(new Rect(width - 18, 2, 16, 16), "X")) { open = false; @@ -190,9 +193,9 @@ private void AmmoSelectorWindow(int id) lastGUIstring = String.Empty; } line++; - GUI.Label(new Rect(margin, line * buttonHeight, width - 2 * margin, buttonHeight), Localizer.Format("#LOC_BDArmory_Ammo_Weapon") + " " + selectedWeapon.GetShortName(), labelStyle); + GUI.Label(new Rect(margin, line * buttonHeight, width - 2 * margin, buttonHeight), StringUtils.Localize("#LOC_BDArmory_Ammo_Weapon") + " " + selectedWeapon.GetShortName(), labelStyle); line++; - GUI.Label(new Rect(margin, line * buttonHeight, width - 2 * margin, buttonHeight), Localizer.Format("#LOC_BDArmory_Ammo_Belt"), labelStyle); + GUI.Label(new Rect(margin, line * buttonHeight, width - 2 * margin, buttonHeight), StringUtils.Localize("#LOC_BDArmory_Ammo_Belt"), labelStyle); line += 1.2f; labelLines = Mathf.Clamp(Mathf.CeilToInt(labelString.Length / 50), 1, 4); BeginArea(new Rect(margin, line * buttonHeight, width - 2 * margin, labelLines * buttonHeight)); @@ -233,7 +236,7 @@ private void AmmoSelectorWindow(int id) ammolines += 1.1f; } } - if (GUI.Button(new Rect(margin * 5, (line + labelLines + ammolines) * buttonHeight, (width - (10 * margin)) / 2, buttonHeight), Localizer.Format("#LOC_BDArmory_reset"))) + if (GUI.Button(new Rect(margin * 5, (line + labelLines + ammolines) * buttonHeight, (width - (10 * margin)) / 2, buttonHeight), StringUtils.Localize("#LOC_BDArmory_reset"))) { beltString = String.Empty; GUIstring = String.Empty; @@ -242,7 +245,7 @@ private void AmmoSelectorWindow(int id) labelLines = 1; roundCounter = 1; } - if (GUI.Button(new Rect(((margin * 5) + ((width - (10 * margin)) / 2)), (line + labelLines + ammolines) * buttonHeight, (width - (10 * margin)) / 2, buttonHeight), Localizer.Format("#LOC_BDArmory_save"))) + if (GUI.Button(new Rect(((margin * 5) + ((width - (10 * margin)) / 2)), (line + labelLines + ammolines) * buttonHeight, (width - (10 * margin)) / 2, buttonHeight), StringUtils.Localize("#LOC_BDArmory_save"))) { save = true; open = false; @@ -251,7 +254,7 @@ private void AmmoSelectorWindow(int id) height = Mathf.Lerp(height, (line + labelLines + ammolines) * buttonHeight, 0.15f); windowRect.height = height; GUI.DragWindow(); - BDGUIUtils.RepositionWindow(ref windowRect); + GUIUtils.RepositionWindow(ref windowRect); } private void Awake() diff --git a/BDArmory/UI/BDArmoryAIGUI.cs b/BDArmory/UI/BDArmoryAIGUI.cs index 6d5f5249f..3e4315f48 100644 --- a/BDArmory/UI/BDArmoryAIGUI.cs +++ b/BDArmory/UI/BDArmoryAIGUI.cs @@ -1,15 +1,13 @@ -using System.Collections; -using BDArmory.Core; -using BDArmory.Control; -using BDArmory.Modules; -using UnityEngine; -using KSP.Localization; using KSP.UI.Screens; -using static UnityEngine.GUILayout; -using System; using System.Collections.Generic; -using static BDArmory.UI.VesselSpawnerWindow; -using BDArmory.Misc; +using System.Collections; +using System; +using UnityEngine; +using static UnityEngine.GUILayout; + +using BDArmory.Control; +using BDArmory.Settings; +using BDArmory.Utils; namespace BDArmory.UI { @@ -21,17 +19,21 @@ public class BDArmoryAIGUI : MonoBehaviour public static bool contextTipsEnabled = false; public static bool NumFieldsEnabled = false; public static bool windowBDAAIGUIEnabled; + internal static bool resizingWindow = false; + internal static int _guiCheckIndex = -1; public static ApplicationLauncherButton button; float WindowWidth = 500; - float WindowHeight = 250; + float WindowHeight = 350; + float contentHeight = 0; float height = 0; float ColumnWidth = 350; float _buttonSize = 26; float _windowMargin = 4; float contentTop = 10; float entryHeight = 20; + float labelWidth = 200; bool showPID; bool showAltitude; bool showSpeed; @@ -40,6 +42,7 @@ public class BDArmoryAIGUI : MonoBehaviour bool showTerrain; bool showRam; bool showMisc; + bool fixedAutoTuneFields = false; int Drivertype = 0; int broadsideDir = 0; @@ -64,12 +67,14 @@ public class BDArmoryAIGUI : MonoBehaviour GUIStyle Title; GUIStyle contextLabel; GUIStyle infoLinkStyle; + GUIStyle inputFieldStyle; + bool stylesConfigured = false; + void Awake() { if (Instance != null) Destroy(Instance); Instance = this; - BDArmorySetup.WindowRectAI = new Rect(BDArmorySetup.WindowRectAI.x, BDArmorySetup.WindowRectAI.y, WindowWidth, WindowHeight); } void Start() @@ -84,36 +89,9 @@ void Start() } if (BDArmorySettings.AI_TOOLBAR_BUTTON) AddToolbarButton(); - if (button != null) button.enabled = windowBDAAIGUIEnabled; - - Label = new GUIStyle(); - Label.alignment = TextAnchor.UpperLeft; - Label.normal.textColor = Color.white; - - rightLabel = new GUIStyle(); - rightLabel.alignment = TextAnchor.UpperRight; - rightLabel.normal.textColor = Color.white; - - contextLabel = new GUIStyle(); - contextLabel.alignment = TextAnchor.UpperCenter; - contextLabel.normal.textColor = Color.white; - - BoldLabel = new GUIStyle(); - BoldLabel.alignment = TextAnchor.UpperLeft; - BoldLabel.fontStyle = FontStyle.Bold; - BoldLabel.normal.textColor = Color.white; - - Title = new GUIStyle(); - Title.normal.textColor = BDArmorySetup.BDGuiSkin.window.normal.textColor; - Title.font = BDArmorySetup.BDGuiSkin.window.font; - Title.fontSize = BDArmorySetup.BDGuiSkin.window.fontSize; - Title.fontStyle = BDArmorySetup.BDGuiSkin.window.fontStyle; - Title.alignment = TextAnchor.UpperCenter; - - infoLinkStyle = new GUIStyle(BDArmorySetup.BDGuiSkin.label); - infoLinkStyle.alignment = TextAnchor.UpperLeft; - infoLinkStyle.normal.textColor = Color.white; + BDArmorySetup.WindowRectAI = new Rect(BDArmorySetup.WindowRectAI.x, BDArmorySetup.WindowRectAI.y, WindowWidth, BDArmorySetup.WindowRectAI.height); + WindowHeight = Mathf.Max(BDArmorySetup.WindowRectAI.height, 305); if (HighLogic.LoadedSceneIsFlight) { @@ -125,6 +103,7 @@ void Start() GameEvents.onEditorPartPlaced.Add(OnEditorPartPlacedEvent); //do per part placement instead of calling a findModule call every time *anything* changes on thevessel GameEvents.onEditorPartDeleted.Add(OnEditorPartDeletedEvent); } + if (_guiCheckIndex < 0) _guiCheckIndex = GUIUtils.RegisterGUIRect(BDArmorySetup.WindowRectAI); } public void AddToolbarButton() { @@ -143,27 +122,38 @@ IEnumerator ToolbarButtonRoutine() { if (buttonSetup) yield break; if (!HighLogic.LoadedSceneIsFlight && !HighLogic.LoadedSceneIsEditor) yield break; - while (!ApplicationLauncher.Ready) - { - yield return null; - } + yield return new WaitUntil(() => ApplicationLauncher.Ready && BDArmorySetup.toolbarButtonAdded); // Wait until after the main BDA toolbar button. if (!buttonSetup) { Texture buttonTexture = GameDatabase.Instance.GetTexture(BDArmorySetup.textureDir + "icon_ai", false); - button = ApplicationLauncher.Instance.AddModApplication(ShowToolbarGUI, HideToolbarGUI, Dummy, Dummy, Dummy, Dummy, ApplicationLauncher.AppScenes.SPH | ApplicationLauncher.AppScenes.VAB | ApplicationLauncher.AppScenes.FLIGHT, buttonTexture); + button = ApplicationLauncher.Instance.AddModApplication(ShowAIGUI, HideAIGUI, Dummy, Dummy, Dummy, Dummy, ApplicationLauncher.AppScenes.SPH | ApplicationLauncher.AppScenes.VAB | ApplicationLauncher.AppScenes.FLIGHT, buttonTexture); buttonSetup = true; + if (windowBDAAIGUIEnabled) button.SetTrue(false); } } - public void ShowToolbarGUI() + public void ToggleAIGUI() + { + if (windowBDAAIGUIEnabled) HideAIGUI(); + else ShowAIGUI(); + } + + public void ShowAIGUI() { windowBDAAIGUIEnabled = true; + GUIUtils.SetGUIRectVisible(_guiCheckIndex, windowBDAAIGUIEnabled); + if (HighLogic.LoadedSceneIsFlight) Instance.GetAI(); // Call via Instance to avoid issue with the toolbar button holding a reference to a null gameobject causing an NRE when starting a coroutine. + else Instance.GetAIEditor(); + if (button != null) button.SetTrue(false); } - public void HideToolbarGUI() + public void HideAIGUI() { windowBDAAIGUIEnabled = false; + GUIUtils.SetGUIRectVisible(_guiCheckIndex, windowBDAAIGUIEnabled); + BDAWindowSettingsField.Save(); // Save window settings. + if (button != null) button.SetFalse(false); } void Dummy() @@ -175,13 +165,14 @@ void Update() { if (BDInputUtils.GetKeyDown(BDInputSettingsFields.GUI_AI_TOGGLE)) { - windowBDAAIGUIEnabled = !windowBDAAIGUIEnabled; + ToggleAIGUI(); } } } void OnVesselChange(Vessel v) { + if (!windowBDAAIGUIEnabled) return; if (v == null) return; if (v.isActiveVessel) { @@ -237,11 +228,20 @@ void GetAI() { // Make sure we're synced between the sliders and input fields in case something changed just before the switch. SyncInputFieldsNow(NumFieldsEnabled); + if (_getAICoroutine != null) StopCoroutine(_getAICoroutine); + _getAICoroutine = StartCoroutine(GetAICoroutine()); + } + Coroutine _getAICoroutine; + IEnumerator GetAICoroutine() + { // Then, reset all the fields as this is only occurring on vessel change, so they need resetting anyway. ActivePilot = null; ActiveDriver = null; inputFields = null; - if (FlightGlobals.ActiveVessel == null) return; + var tic = Time.time; + if (FlightGlobals.ActiveVessel == null) + yield return new WaitUntilFixed(() => FlightGlobals.ActiveVessel != null || Time.time - tic > 1); // Give it up to a second to find the active vessel. + if (FlightGlobals.ActiveVessel == null) yield break; // Now, get the new AI and update stuff. ActivePilot = VesselModuleRegistry.GetBDModulePilotAI(FlightGlobals.ActiveVessel, true); if (ActivePilot == null) @@ -259,30 +259,43 @@ void GetAI() SetChooseOptionSliders(); } } + void GetAIEditor() { - if (EditorLogic.fetch.ship == null) return; - foreach (var p in EditorLogic.fetch.ship.Parts) // Take the AIs in the order they were placed on the ship. + if (_getAIEditorCoroutine != null) StopCoroutine(_getAIEditorCoroutine); + _getAIEditorCoroutine = StartCoroutine(GetAIEditorCoroutine()); + } + Coroutine _getAIEditorCoroutine; + IEnumerator GetAIEditorCoroutine() + { + var tic = Time.time; + if (EditorLogic.fetch.ship == null || EditorLogic.fetch.ship.Parts == null) + yield return new WaitUntilFixed(() => (EditorLogic.fetch.ship != null && EditorLogic.fetch.ship.Parts != null) || Time.time - tic > 1); // Give it up to a second to find the editor ship and parts. + if (EditorLogic.fetch.ship != null && EditorLogic.fetch.ship.Parts != null) { - foreach (var AI in p.FindModulesImplementing()) - { - if (AI == null) continue; - if (AI == ActivePilot) return; // We found the current ActivePilot! - ActivePilot = AI; - inputFields = null; // Reset the input fields to the current AI. - SetInputFields(ActivePilot.GetType()); - return; - } - foreach (var AI in p.FindModulesImplementing()) + foreach (var p in EditorLogic.fetch.ship.Parts) // Take the AIs in the order they were placed on the ship. { - if (AI == null) continue; - if (AI == ActiveDriver) return; // We found the current ActiveDriver! - ActiveDriver = AI; - inputFields = null; // Reset the input fields to the current AI. - SetInputFields(ActiveDriver.GetType()); - return; + foreach (var AI in p.FindModulesImplementing()) + { + if (AI == null) continue; + if (AI == ActivePilot) yield break; // We found the current ActivePilot! + ActivePilot = AI; + inputFields = null; // Reset the input fields to the current AI. + SetInputFields(ActivePilot.GetType()); + yield break; + } + foreach (var AI in p.FindModulesImplementing()) + { + if (AI == null) continue; + if (AI == ActiveDriver) yield break; // We found the current ActiveDriver! + ActiveDriver = AI; + inputFields = null; // Reset the input fields to the current AI. + SetInputFields(ActiveDriver.GetType()); + yield break; + } } } + // No AIs were found, clear everything. ActivePilot = null; ActiveDriver = null; @@ -303,44 +316,55 @@ void SetInputFields(Type AIType) inputFields = new Dictionary { { "steerMult", gameObject.AddComponent().Initialise(0, ActivePilot.steerMult, 0.1, 20) }, { "steerKiAdjust", gameObject.AddComponent().Initialise(0, ActivePilot.steerKiAdjust, 0.01, 1) }, - { "steerDamping", gameObject.AddComponent().Initialise(0, ActivePilot.steerDamping, 1, 8) }, + { "steerDamping", gameObject.AddComponent().Initialise(0, ActivePilot.steerDamping, 0.1, 8) }, - { "DynamicDampingMin", gameObject.AddComponent().Initialise(0, ActivePilot.DynamicDampingMin, 1, 8) }, - { "DynamicDampingMax", gameObject.AddComponent().Initialise(0, ActivePilot.DynamicDampingMax, 1, 8) }, + { "DynamicDampingMin", gameObject.AddComponent().Initialise(0, ActivePilot.DynamicDampingMin, 0.1, 8) }, + { "DynamicDampingMax", gameObject.AddComponent().Initialise(0, ActivePilot.DynamicDampingMax, 0.1, 8) }, { "dynamicSteerDampingFactor", gameObject.AddComponent().Initialise(0, ActivePilot.dynamicSteerDampingFactor, 0.1, 10) }, - { "DynamicDampingPitchMin", gameObject.AddComponent().Initialise(0, ActivePilot.DynamicDampingPitchMin, 1, 8) }, - { "DynamicDampingPitchMax", gameObject.AddComponent().Initialise(0, ActivePilot.DynamicDampingPitchMax, 1, 8) }, + { "DynamicDampingPitchMin", gameObject.AddComponent().Initialise(0, ActivePilot.DynamicDampingPitchMin, 0.1, 8) }, + { "DynamicDampingPitchMax", gameObject.AddComponent().Initialise(0, ActivePilot.DynamicDampingPitchMax, 0.1, 8) }, { "dynamicSteerDampingPitchFactor", gameObject.AddComponent().Initialise(0, ActivePilot.dynamicSteerDampingPitchFactor, 0.1, 10) }, - { "DynamicDampingYawMin", gameObject.AddComponent().Initialise(0, ActivePilot.DynamicDampingYawMin, 1, 8) }, - { "DynamicDampingYawMax", gameObject.AddComponent().Initialise(0, ActivePilot.DynamicDampingYawMax, 1, 8) }, + { "DynamicDampingYawMin", gameObject.AddComponent().Initialise(0, ActivePilot.DynamicDampingYawMin, 0.1, 8) }, + { "DynamicDampingYawMax", gameObject.AddComponent().Initialise(0, ActivePilot.DynamicDampingYawMax, 0.1, 8) }, { "dynamicSteerDampingYawFactor", gameObject.AddComponent().Initialise(0, ActivePilot.dynamicSteerDampingYawFactor, 0.1, 10) }, - { "DynamicDampingRollMin", gameObject.AddComponent().Initialise(0, ActivePilot.DynamicDampingRollMax, 1, 8) }, - { "DynamicDampingRollMax", gameObject.AddComponent().Initialise(0, ActivePilot.DynamicDampingRollMax, 1, 8) }, + { "DynamicDampingRollMin", gameObject.AddComponent().Initialise(0, ActivePilot.DynamicDampingRollMin, 0.1, 8) }, + { "DynamicDampingRollMax", gameObject.AddComponent().Initialise(0, ActivePilot.DynamicDampingRollMax, 0.1, 8) }, { "dynamicSteerDampingRollFactor", gameObject.AddComponent().Initialise(0, ActivePilot.dynamicSteerDampingRollFactor, 0.1, 10) }, - { "defaultAltitude", gameObject.AddComponent().Initialise(0, ActivePilot.defaultAltitude, 100, 15000) }, + { "autoTuningOptionNumSamples", gameObject.AddComponent().Initialise(0, ActivePilot.autoTuningOptionNumSamples, 1, 10) }, + { "autoTuningOptionFastResponseRelevance", gameObject.AddComponent().Initialise(0, ActivePilot.autoTuningOptionFastResponseRelevance, 0, 0.5) }, + { "autoTuningOptionInitialLearningRate", gameObject.AddComponent().Initialise(0, ActivePilot.autoTuningOptionInitialLearningRate, 1e-3, 1) }, + { "autoTuningAltitude", gameObject.AddComponent().Initialise(0, ActivePilot.autoTuningAltitude, 50, 5000) }, + { "autoTuningSpeed", gameObject.AddComponent().Initialise(0, ActivePilot.autoTuningSpeed, 50, 800) }, + + { "defaultAltitude", gameObject.AddComponent().Initialise(0, ActivePilot.defaultAltitude, 50, 15000) }, { "minAltitude", gameObject.AddComponent().Initialise(0, ActivePilot.minAltitude, 25, 6000) }, { "maxAltitude", gameObject.AddComponent().Initialise(0, ActivePilot.maxAltitude, 100, 15000) }, - { "maxSpeed", gameObject.AddComponent().Initialise(0, ActivePilot.maxSpeed, 20, 800) }, + { "maxSpeed", gameObject.AddComponent().Initialise(0, ActivePilot.maxSpeed, 20, (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 55)? 600 :800) }, { "takeOffSpeed", gameObject.AddComponent().Initialise(0, ActivePilot.takeOffSpeed, 10, 200) }, { "minSpeed", gameObject.AddComponent().Initialise(0, ActivePilot.minSpeed, 10, 200) }, { "strafingSpeed", gameObject.AddComponent().Initialise(0, ActivePilot.strafingSpeed, 10, 200) }, { "idleSpeed", gameObject.AddComponent().Initialise(0, ActivePilot.idleSpeed, 10, 200) }, { "ABPriority", gameObject.AddComponent().Initialise(0, ActivePilot.ABPriority, 0, 100) }, + { "ABOverrideThreshold", gameObject.AddComponent().Initialise(0, ActivePilot.ABOverrideThreshold, 0, 200) }, { "maxSteer", gameObject.AddComponent().Initialise(0, ActivePilot.maxSteer, 0.1, 1) }, { "lowSpeedSwitch", gameObject.AddComponent().Initialise(0, ActivePilot.lowSpeedSwitch, 10, 500) }, { "maxSteerAtMaxSpeed", gameObject.AddComponent().Initialise(0, ActivePilot.maxSteerAtMaxSpeed, 0.1, 1) }, { "cornerSpeed", gameObject.AddComponent().Initialise(0, ActivePilot.cornerSpeed, 10, 500) }, - { "maxBank", gameObject.AddComponent().Initialise(0, ActivePilot.maxBank, 10, 180) }, + { "altitudeSteerLimiterFactor", gameObject.AddComponent().Initialise(0, ActivePilot.altitudeSteerLimiterFactor, -1, 1) }, + { "altitudeSteerLimiterAltitude", gameObject.AddComponent().Initialise(0, ActivePilot.altitudeSteerLimiterAltitude, 100, 10000) }, + { "maxBank", gameObject.AddComponent().Initialise(0, ActivePilot.maxBank, 10, (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 55)? 40 : 180) }, { "waypointPreRollTime", gameObject.AddComponent().Initialise(0, ActivePilot.waypointPreRollTime, 0, 2) }, { "waypointYawAuthorityTime", gameObject.AddComponent().Initialise(0, ActivePilot.waypointYawAuthorityTime, 0, 10) }, { "maxAllowedGForce", gameObject.AddComponent().Initialise(0, ActivePilot.maxAllowedGForce, 2, 45) }, - { "maxAllowedAoA", gameObject.AddComponent().Initialise(0, ActivePilot.maxAllowedAoA, 0, 85) }, + { "maxAllowedAoA", gameObject.AddComponent().Initialise(0, ActivePilot.maxAllowedAoA, 0, 90) }, + { "postStallAoA", gameObject.AddComponent().Initialise(0, ActivePilot.postStallAoA, 0, (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 55)? 0 : 90) }, + { "ImmelmannTurnAngle", gameObject.AddComponent().Initialise(0, ActivePilot.ImmelmannTurnAngle, 0, 90) }, { "minEvasionTime", gameObject.AddComponent().Initialise(0, ActivePilot.minEvasionTime, 0, 1) }, { "evasionNonlinearity", gameObject.AddComponent().Initialise(0, ActivePilot.evasionNonlinearity, 0, 10) }, @@ -348,18 +372,22 @@ void SetInputFields(Type AIType) { "evasionTimeThreshold", gameObject.AddComponent().Initialise(0, ActivePilot.evasionTimeThreshold, 0, 1) }, { "collisionAvoidanceThreshold", gameObject.AddComponent().Initialise(0, ActivePilot.collisionAvoidanceThreshold, 0, 50) }, { "vesselCollisionAvoidanceLookAheadPeriod", gameObject.AddComponent().Initialise(0, ActivePilot.vesselCollisionAvoidanceLookAheadPeriod, 0, 3) }, - { "vesselCollisionAvoidanceStrength", gameObject.AddComponent().Initialise(0, ActivePilot.vesselCollisionAvoidanceStrength, 0, 2) }, + { "vesselCollisionAvoidanceStrength", gameObject.AddComponent().Initialise(0, ActivePilot.vesselCollisionAvoidanceStrength, 0, 4) }, { "vesselStandoffDistance", gameObject.AddComponent().Initialise(0, ActivePilot.vesselStandoffDistance, 0, 1000) }, - // { "extendMult", gameObject.AddComponent().Initialise(0, ActivePilot.extendMult, 0, 2) }, { "extendDistanceAirToAir", gameObject.AddComponent().Initialise(0, ActivePilot.extendDistanceAirToAir, 0, 2000) }, + { "extendAngleAirToAir", gameObject.AddComponent().Initialise(0, ActivePilot.extendAngleAirToAir, -10, 45) }, { "extendDistanceAirToGroundGuns", gameObject.AddComponent().Initialise(0, ActivePilot.extendDistanceAirToGroundGuns, 0, 5000) }, { "extendDistanceAirToGround", gameObject.AddComponent().Initialise(0, ActivePilot.extendDistanceAirToGround, 0, 5000) }, { "extendTargetVel", gameObject.AddComponent().Initialise(0, ActivePilot.extendTargetVel, 0, 2) }, { "extendTargetAngle", gameObject.AddComponent().Initialise(0, ActivePilot.extendTargetAngle, 0, 180) }, { "extendTargetDist", gameObject.AddComponent().Initialise(0, ActivePilot.extendTargetDist, 0, 5000) }, + { "extendAbortTime", gameObject.AddComponent().Initialise(0, ActivePilot.extendAbortTime, 5, 30) }, { "turnRadiusTwiddleFactorMin", gameObject.AddComponent().Initialise(0, ActivePilot.turnRadiusTwiddleFactorMin, 0.1, 5) }, { "turnRadiusTwiddleFactorMax", gameObject.AddComponent().Initialise(0, ActivePilot.turnRadiusTwiddleFactorMax, 0.1, 5) }, + { "terrainAvoidanceCriticalAngle", gameObject.AddComponent().Initialise(0, ActivePilot.terrainAvoidanceCriticalAngle, 90f, 180f) }, + { "controlSurfaceDeploymentTime", gameObject.AddComponent().Initialise(0, ActivePilot.controlSurfaceDeploymentTime, 0f, 4f) }, + { "waypointTerrainAvoidance", gameObject.AddComponent().Initialise(0, ActivePilot.waypointTerrainAvoidance, 0, 1) }, { "controlSurfaceLag", gameObject.AddComponent().Initialise(0, ActivePilot.controlSurfaceLag, 0, 0.2) }, }; @@ -409,26 +437,31 @@ void SetInputFields(Type AIType) inputFields["minAltitude"].maxValue = ActivePilot.UpToEleven ? 60000 : 6000; inputFields["maxAltitude"].maxValue = ActivePilot.UpToEleven ? 100000 : 15000; - inputFields["maxSpeed"].maxValue = ActivePilot.UpToEleven ? 3000 : 800; + if (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 55) inputFields["maxSpeed"].maxValue = 600; + else inputFields["maxSpeed"].maxValue = ActivePilot.UpToEleven ? 3000 : 800; inputFields["takeOffSpeed"].maxValue = ActivePilot.UpToEleven ? 2000 : 200; inputFields["minSpeed"].maxValue = ActivePilot.UpToEleven ? 2000 : 200; inputFields["idleSpeed"].maxValue = ActivePilot.UpToEleven ? 3000 : 200; inputFields["maxAllowedGForce"].maxValue = ActivePilot.UpToEleven ? 1000 : 45; - inputFields["maxAllowedAoA"].maxValue = ActivePilot.UpToEleven ? 180 : 85; + inputFields["maxAllowedAoA"].maxValue = ActivePilot.UpToEleven ? 180 : 90; + inputFields["postStallAoA"].maxValue = ActivePilot.UpToEleven ? 180 : (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 55) ? 0 : 90; inputFields["minEvasionTime"].maxValue = ActivePilot.UpToEleven ? 10 : 1; inputFields["evasionNonlinearity"].maxValue = ActivePilot.UpToEleven ? 90 : 10; inputFields["evasionThreshold"].maxValue = ActivePilot.UpToEleven ? 300 : 100; inputFields["evasionTimeThreshold"].maxValue = ActivePilot.UpToEleven ? 1 : 3; inputFields["vesselStandoffDistance"].maxValue = ActivePilot.UpToEleven ? 5000 : 1000; - // inputFields["extendMult"].maxValue = ActivePilot.UpToEleven ? 200 : 2; inputFields["extendDistanceAirToAir"].maxValue = ActivePilot.UpToEleven ? 20000 : 2000; + inputFields["extendAngleAirToAir"].maxValue = ActivePilot.UpToEleven ? 90 : 45; + inputFields["extendAngleAirToAir"].minValue = ActivePilot.UpToEleven ? -90 : -10; inputFields["extendDistanceAirToGroundGuns"].maxValue = ActivePilot.UpToEleven ? 20000 : 5000; inputFields["extendDistanceAirToGround"].maxValue = ActivePilot.UpToEleven ? 20000 : 5000; inputFields["turnRadiusTwiddleFactorMin"].maxValue = ActivePilot.UpToEleven ? 10 : 5; inputFields["turnRadiusTwiddleFactorMax"].maxValue = ActivePilot.UpToEleven ? 10 : 5; + inputFields["controlSurfaceDeploymentTime"].maxValue = ActivePilot.UpToEleven ? 10 : 4; + inputFields["controlSurfaceLag"].maxValue = ActivePilot.UpToEleven ? 1 : 0.2f; } } @@ -555,9 +588,48 @@ void OnGUI() if (!BDArmorySetup.GAME_UI_ENABLED) return; if (!windowBDAAIGUIEnabled || (!HighLogic.LoadedSceneIsFlight && !HighLogic.LoadedSceneIsEditor)) return; - //BDArmorySetup.WindowRectAI = new Rect(BDArmorySetup.WindowRectAI.x, BDArmorySetup.WindowRectAI.y, WindowWidth, WindowHeight); - BDArmorySetup.WindowRectAI = GUI.Window(GetInstanceID(), BDArmorySetup.WindowRectAI, WindowRectAI, "", BDArmorySetup.BDGuiSkin.window);//"BDA Weapon Manager" - BDGUIUtils.UseMouseEventInRect(BDArmorySetup.WindowRectAI); + if (!stylesConfigured) ConfigureStyles(); + if (HighLogic.LoadedSceneIsFlight) BDArmorySetup.SetGUIOpacity(); + if (Event.current.type == EventType.MouseUp && resizingWindow) { resizingWindow = false; } + BDArmorySetup.WindowRectAI = GUI.Window(GUIUtility.GetControlID(FocusType.Passive), BDArmorySetup.WindowRectAI, WindowRectAI, "", BDArmorySetup.BDGuiSkin.window);//"BDA Weapon Manager" + if (HighLogic.LoadedSceneIsFlight) BDArmorySetup.SetGUIOpacity(false); + GUIUtils.RepositionWindow(ref BDArmorySetup.WindowRectAI); + GUIUtils.UpdateGUIRect(BDArmorySetup.WindowRectAI, _guiCheckIndex); + GUIUtils.UseMouseEventInRect(BDArmorySetup.WindowRectAI); + } + + void ConfigureStyles() + { + Label = new GUIStyle(); + Label.alignment = TextAnchor.UpperLeft; + Label.normal.textColor = Color.white; + + rightLabel = new GUIStyle(); + rightLabel.alignment = TextAnchor.UpperRight; + rightLabel.normal.textColor = Color.white; + + contextLabel = new GUIStyle(Label); + + inputFieldStyle = new GUIStyle(GUI.skin.textField); + inputFieldStyle.alignment = TextAnchor.UpperRight; + + BoldLabel = new GUIStyle(); + BoldLabel.alignment = TextAnchor.UpperLeft; + BoldLabel.fontStyle = FontStyle.Bold; + BoldLabel.normal.textColor = Color.white; + + Title = new GUIStyle(); + Title.normal.textColor = BDArmorySetup.BDGuiSkin.window.normal.textColor; + Title.font = BDArmorySetup.BDGuiSkin.window.font; + Title.fontSize = BDArmorySetup.BDGuiSkin.window.fontSize; + Title.fontStyle = BDArmorySetup.BDGuiSkin.window.fontStyle; + Title.alignment = TextAnchor.UpperCenter; + + infoLinkStyle = new GUIStyle(BDArmorySetup.BDGuiSkin.label); + infoLinkStyle.alignment = TextAnchor.UpperLeft; + infoLinkStyle.normal.textColor = Color.white; + + stylesConfigured = true; } float pidHeight; @@ -581,19 +653,23 @@ Rect SubsectionRect(float indent, float line) Rect SettinglabelRect(float indent, float lines) { - return new Rect(indent, (lines * entryHeight), 85, entryHeight); + return new Rect(indent, (lines * entryHeight), labelWidth, entryHeight); } Rect SettingSliderRect(float indent, float lines, float contentWidth) { - return new Rect(indent + 150, (lines * entryHeight), contentWidth - (indent * 2) - (150 + 10), entryHeight); + return new Rect(indent + labelWidth, (lines + 0.2f) * entryHeight, contentWidth - (indent * 2) - labelWidth, entryHeight); } Rect SettingTextRect(float indent, float lines, float contentWidth) { - return new Rect(indent + 250, (lines * entryHeight), contentWidth - (indent * 2) - (250 + 10 + 100), entryHeight); + return new Rect(indent + labelWidth, lines * entryHeight, contentWidth - (indent * 2) - labelWidth, entryHeight); } Rect ContextLabelRect(float indent, float lines) { - return new Rect(150 + indent, (lines * entryHeight), 85, entryHeight); + return new Rect(labelWidth + indent, lines * entryHeight, 100, entryHeight); + } + Rect ContextLabelRectRight(float indent, float lines, float contentWidth) + { + return new Rect(contentWidth - 100 - 2 * indent, lines * entryHeight, 100, entryHeight); } Rect ToggleButtonRect(float indent, float lines, float contentWidth) @@ -601,6 +677,12 @@ Rect ToggleButtonRect(float indent, float lines, float contentWidth) return new Rect(indent, (lines * entryHeight), contentWidth - (2 * indent), entryHeight); } + Rect ToggleButtonRects(float indent, float lines, float pos, float of, float contentWidth) + { + var gap = indent / 2f; + return new Rect(indent + pos / of * (contentWidth - gap * (of - 1f) - 2f * indent) + pos * gap, lines * entryHeight, 1f / of * (contentWidth - gap * (of - 1f) - 2f * indent), entryHeight); + } + void WindowRectAI(int windowID) { float line = 0; @@ -610,8 +692,7 @@ void WindowRectAI(int windowID) GUI.DragWindow(new Rect(_windowMargin + _buttonSize * 6, 0, (ColumnWidth * 2) - (2 * _windowMargin) - (10 * _buttonSize), _windowMargin + _buttonSize)); - GUI.Label(new Rect(100, contentTop, contentWidth, entryHeight), - Localizer.Format("#LOC_BDArmory_AIWindow_title"), Title);// "No AI found." + GUI.Label(new Rect(100, contentTop, contentWidth, entryHeight), StringUtils.Localize("#LOC_BDArmory_AIWindow_title"), Title);// "No AI found." line += 1.25f; line += 0.25f; @@ -620,7 +701,7 @@ void WindowRectAI(int windowID) GUIStyle buttonStyle = windowBDAAIGUIEnabled ? BDArmorySetup.BDGuiSkin.button : BDArmorySetup.BDGuiSkin.box; if (GUI.Button(TitleButtonRect(1), "X", buttonStyle)) { - windowBDAAIGUIEnabled = !windowBDAAIGUIEnabled; + ToggleAIGUI(); } //Infolink button @@ -648,11 +729,12 @@ void WindowRectAI(int windowID) if (ActivePilot == null && ActiveDriver == null) { GUI.Label(new Rect(leftIndent, contentTop + (1.75f * entryHeight), contentWidth, entryHeight), - Localizer.Format("#LOC_BDArmory_AIWindow_NoAI"), Title);// "No AI found." + StringUtils.Localize("#LOC_BDArmory_AIWindow_NoAI"), Title);// "No AI found." line += 4; } else { + height = Mathf.Lerp(height, contentHeight, 0.15f); if (ActivePilot != null) { GUIStyle saveStyle = BDArmorySetup.BDGuiSkin.button; @@ -671,39 +753,39 @@ void WindowRectAI(int windowID) } showPID = GUI.Toggle(SubsectionRect(leftIndent, line), - showPID, Localizer.Format("#LOC_BDArmory_PilotAI_PID"), showPID ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"PiD" + showPID, StringUtils.Localize("#LOC_BDArmory_PilotAI_PID"), showPID ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"PiD" line += 1.5f; showAltitude = GUI.Toggle(SubsectionRect(leftIndent, line), - showAltitude, Localizer.Format("#LOC_BDArmory_PilotAI_Altitudes"), showAltitude ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Altitude" + showAltitude, StringUtils.Localize("#LOC_BDArmory_PilotAI_Altitudes"), showAltitude ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Altitude" line += 1.5f; showSpeed = GUI.Toggle(SubsectionRect(leftIndent, line), - showSpeed, Localizer.Format("#LOC_BDArmory_PilotAI_Speeds"), showSpeed ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Speed" + showSpeed, StringUtils.Localize("#LOC_BDArmory_PilotAI_Speeds"), showSpeed ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Speed" line += 1.5f; showControl = GUI.Toggle(SubsectionRect(leftIndent, line), - showControl, Localizer.Format("#LOC_BDArmory_AIWindow_ControlLimits"), showControl ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Control" + showControl, StringUtils.Localize("#LOC_BDArmory_AIWindow_ControlLimits"), showControl ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Control" line += 1.5f; showEvade = GUI.Toggle(SubsectionRect(leftIndent, line), - showEvade, Localizer.Format("#LOC_BDArmory_AIWindow_EvadeExtend"), showEvade ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Evasion" + showEvade, StringUtils.Localize("#LOC_BDArmory_AIWindow_EvadeExtend"), showEvade ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Evasion" line += 1.5f; showTerrain = GUI.Toggle(SubsectionRect(leftIndent, line), - showTerrain, Localizer.Format("#LOC_BDArmory_AIWindow_Terrain"), showTerrain ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Terrain" + showTerrain, StringUtils.Localize("#LOC_BDArmory_AIWindow_Terrain"), showTerrain ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Terrain" line += 1.5f; showRam = GUI.Toggle(SubsectionRect(leftIndent, line), - showRam, Localizer.Format("#LOC_BDArmory_PilotAI_Ramming"), showRam ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"rammin" + showRam, StringUtils.Localize("#LOC_BDArmory_PilotAI_Ramming"), showRam ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Ramming" line += 1.5f; showMisc = GUI.Toggle(SubsectionRect(leftIndent, line), - showMisc, Localizer.Format("#LOC_BDArmory_PilotAI_Misc"), showMisc ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Misc" + showMisc, StringUtils.Localize("#LOC_BDArmory_PilotAI_Misc"), showMisc ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Misc" line += 1.5f; ActivePilot.UpToEleven = GUI.Toggle(SubsectionRect(leftIndent, line), - ActivePilot.UpToEleven, ActivePilot.UpToEleven ? Localizer.Format("#LOC_BDArmory_UnclampTuning_enabledText") : Localizer.Format("#LOC_BDArmory_UnclampTuning_disabledText"), ActivePilot.UpToEleven ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Misc" + ActivePilot.UpToEleven, ActivePilot.UpToEleven ? StringUtils.Localize("#LOC_BDArmory_UnclampTuning_enabledText") : StringUtils.Localize("#LOC_BDArmory_UnclampTuning_disabledText"), ActivePilot.UpToEleven ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Misc" if (ActivePilot.UpToEleven != oldClamp) { SetInputFields(ActivePilot.GetType()); @@ -719,1332 +801,1483 @@ void WindowRectAI(int windowID) float ramLines = 0; float miscLines = 0; - if (height < WindowHeight) - { - height = WindowHeight - (entryHeight * 1.5f); - } - if (infoLinkEnabled) { windowColumns = 3; - GUI.Label(new Rect(leftIndent + (ColumnWidth * 2), (contentTop), ColumnWidth - (leftIndent), entryHeight), Localizer.Format("#LOC_BDArmory_AIWindow_infoLink"), Title);//"infolink" - BeginArea(new Rect(leftIndent + (ColumnWidth * 2), contentTop + (entryHeight * 1.5f), ColumnWidth - (leftIndent), WindowHeight - (entryHeight * 1.5f) - (2 * contentTop))); - using (var scrollViewScope = new ScrollViewScope(scrollInfoVector, Width(ColumnWidth - (leftIndent)), Height(WindowHeight - (entryHeight * 1.5f) - (2 * contentTop)))) + GUI.Label(new Rect(leftIndent + ColumnWidth * 2, contentTop, ColumnWidth - leftIndent, entryHeight), StringUtils.Localize("#LOC_BDArmory_AIWindow_infoLink"), Title);//"infolink" + BeginArea(new Rect(leftIndent + ColumnWidth * 2, contentTop + entryHeight * 1.5f, ColumnWidth - leftIndent, WindowHeight - entryHeight * 1.5f - 2 * contentTop)); + using (var scrollViewScope = new ScrollViewScope(scrollInfoVector, Width(ColumnWidth - leftIndent), Height(WindowHeight - entryHeight * 1.5f - 2 * contentTop))) { scrollInfoVector = scrollViewScope.scrollPosition; if (showPID) //these autoalign, so if new entries need to be added, they can just be slotted in { - GUILayout.Label(Localizer.Format("#LOC_BDArmory_PilotAI_PID"), BoldLabel, Width(ColumnWidth - (leftIndent * 4) - 20)); //PID label - GUILayout.Label(Localizer.Format("#LOC_BDArmory_AIWindow_PidHelp"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //Pid desc - GUILayout.Label(Localizer.Format("#LOC_BDArmory_AIWindow_PidHelp_SteerMult"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //steer mult desc - GUILayout.Label(Localizer.Format("#LOC_BDArmory_AIWindow_PidHelp_SteerKi"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //steer ki desc. - GUILayout.Label(Localizer.Format("#LOC_BDArmory_AIWindow_PidHelp_Steerdamp"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //steer damp description - GUILayout.Label(Localizer.Format("#LOC_BDArmory_AIWindow_PidHelp_Dyndamp"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //dynamic damping desc + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_PilotAI_PID"), BoldLabel, Width(ColumnWidth - leftIndent * 4 - 20)); //PID label + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_PidHelp"), infoLinkStyle, Width(ColumnWidth - leftIndent * 4 - 20)); //Pid desc + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_PidHelp_SteerMult"), infoLinkStyle, Width(ColumnWidth - leftIndent * 4 - 20)); //steer mult desc + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_PidHelp_SteerKi"), infoLinkStyle, Width(ColumnWidth - leftIndent * 4 - 20)); //steer ki desc. + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_PidHelp_Steerdamp"), infoLinkStyle, Width(ColumnWidth - leftIndent * 4 - 20)); //steer damp description + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_PidHelp_Dyndamp"), infoLinkStyle, Width(ColumnWidth - leftIndent * 4 - 20)); //dynamic damping desc + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_PidHelp_AutoTune") + (ActivePilot.AutoTune ? StringUtils.Localize("#LOC_BDArmory_AIWindow_PidHelp_AutoTune_details") : ""), infoLinkStyle, Width(ColumnWidth - leftIndent * 4 - 20)); //auto-tuning desc } if (showAltitude) { - GUILayout.Label(Localizer.Format("#LOC_BDArmory_PilotAI_Altitudes"), BoldLabel, Width(ColumnWidth - (leftIndent * 4) - 20)); //Altitude label - GUILayout.Label(Localizer.Format("#LOC_BDArmory_AIWindow_AltHelp"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //altitude description - GUILayout.Label(Localizer.Format("#LOC_BDArmory_AIWindow_AltHelp_Def"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //default alt desc - GUILayout.Label(Localizer.Format("#LOC_BDArmory_AIWindow_AltHelp_min"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //min alt desc - GUILayout.Label(Localizer.Format("#LOC_BDArmory_AIWindow_AltHelp_max"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //max alt desc + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_PilotAI_Altitudes"), BoldLabel, Width(ColumnWidth - (leftIndent * 4) - 20)); //Altitude label + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_AltHelp"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //altitude description + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_AltHelp_Def"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //default alt desc + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_AltHelp_min"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //min alt desc + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_AltHelp_max"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //max alt desc } if (showSpeed) { - GUILayout.Label(Localizer.Format("#LOC_BDArmory_PilotAI_Speeds"), BoldLabel, Width(ColumnWidth - (leftIndent * 4) - 20)); //Speed header - GUILayout.Label(Localizer.Format("#LOC_BDArmory_AIWindow_SpeedHelp"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //speed explanation - GUILayout.Label(Localizer.Format("#LOC_BDArmory_AIWindow_SpeedHelp_min"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //min+max speed desc - GUILayout.Label(Localizer.Format("#LOC_BDArmory_AIWindow_SpeedHelp_takeoff"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //takeoff speed - GUILayout.Label(Localizer.Format("#LOC_BDArmory_AIWindow_SpeedHelp_gnd"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //strafe speed - GUILayout.Label(Localizer.Format("#LOC_BDArmory_AIWindow_SpeedHelp_idle"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //idle speed - GUILayout.Label(Localizer.Format("#LOC_BDArmory_AIWindow_SpeedHelp_ABpriority"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //AB priority + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_PilotAI_Speeds"), BoldLabel, Width(ColumnWidth - (leftIndent * 4) - 20)); //Speed header + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_SpeedHelp"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //speed explanation + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_SpeedHelp_min"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //min+max speed desc + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_SpeedHelp_takeoff"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //takeoff speed + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_SpeedHelp_gnd"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //strafe speed + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_SpeedHelp_idle"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //idle speed + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_SpeedHelp_ABpriority"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //AB priority + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_SpeedHelp_ABOverrideThreshold"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //AB override threshold } if (showControl) { - GUILayout.Label(Localizer.Format("#LOC_BDArmory_PilotAI_ControlLimits"), BoldLabel, Width(ColumnWidth - (leftIndent * 4) - 20)); //conrrol header - GUILayout.Label(Localizer.Format("#LOC_BDArmory_AIWindow_ControlHelp"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //control desc - GUILayout.Label(Localizer.Format("#LOC_BDArmory_AIWindow_ControlHelp_limiters"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //low + high speed limiters - GUILayout.Label(Localizer.Format("#LOC_BDArmory_AIWindow_ControlHelp_bank"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //max bank desc - GUILayout.Label(Localizer.Format("#LOC_BDArmory_AIWindow_ControlHelp_clamps"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //max G + max AoA + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_PilotAI_ControlLimits"), BoldLabel, Width(ColumnWidth - (leftIndent * 4) - 20)); //conrrol header + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_ControlHelp"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //control desc + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_ControlHelp_limiters"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //low + high speed limiters + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_ControlHelp_bank"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //max bank desc + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_ControlHelp_clamps"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //max G + max AoA + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_ControlHelp_modeSwitches"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //post-stall + Immelmann turn angle } if (showEvade) { - GUILayout.Label(Localizer.Format("#LOC_BDArmory_PilotAI_EvadeExtend"), BoldLabel, Width(ColumnWidth - (leftIndent * 4) - 20)); //evade header - GUILayout.Label(Localizer.Format("#LOC_BDArmory_AIWindow_EvadeHelp"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //evade description - GUILayout.Label(Localizer.Format("#LOC_BDArmory_AIWindow_EvadeHelp_Evade"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //evade dist/ time/ time threshold - GUILayout.Label(Localizer.Format("#LOC_BDArmory_AIWindow_EvadeHelp_Dodge"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //collision avoid - GUILayout.Label(Localizer.Format("#LOC_BDArmory_AIWindow_EvadeHelp_standoff"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //standoff distance - GUILayout.Label(Localizer.Format("#LOC_BDArmory_AIWindow_EvadeHelp_Extend"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //extend mult - GUILayout.Label(Localizer.Format("#LOC_BDArmory_AIWindow_EvadeHelp_ExtendVars"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //extend target dist/angle/vel - GUILayout.Label(Localizer.Format("#LOC_BDArmory_AIWindow_EvadeHelp_ExtendAngle"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //extend target angle - GUILayout.Label(Localizer.Format("#LOC_BDArmory_AIWindow_EvadeHelp_ExtendDist"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //extend target dist - GUILayout.Label(Localizer.Format("#LOC_BDArmory_AIWindow_EvadeHelp_ExtendVel"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //extend target velocity - GUILayout.Label(Localizer.Format("#LOC_BDArmory_AIWindow_EvadeHelp_Nonlinearity"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //evade/extend nonlinearity + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_PilotAI_EvadeExtend"), BoldLabel, Width(ColumnWidth - (leftIndent * 4) - 20)); //evade header + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_EvadeHelp"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //evade description + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_EvadeHelp_Evade"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //evade dist/ time/ time threshold + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_EvadeHelp_Nonlinearity"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //evade/extend nonlinearity + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_EvadeHelp_Dodge"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //collision avoid + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_EvadeHelp_standoff"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //standoff distance + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_EvadeHelp_Extend"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //extend distances + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_EvadeHelp_ExtendVars"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //extend target dist/angle/vel + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_EvadeHelp_ExtendVel"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //extend target velocity + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_EvadeHelp_ExtendAngle"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //extend target angle + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_EvadeHelp_ExtendDist"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //extend target dist + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_EvadeHelp_ExtendAbortTime"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //extend abort time + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_EvadeHelp_ExtendToggle"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //evade/extend toggle } if (showTerrain) { - GUILayout.Label(Localizer.Format("#LOC_BDArmory_PilotAI_Terrain"), BoldLabel, Width(ColumnWidth - (leftIndent * 4) - 20)); //Terrain avoid header - GUILayout.Label(Localizer.Format("#LOC_BDArmory_AIWindow_TerrainHelp"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //terrain avoid desc + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_PilotAI_Terrain"), BoldLabel, Width(ColumnWidth - (leftIndent * 4) - 20)); //Terrain avoid header + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_TerrainHelp"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //terrain avoid desc } if (showRam) { - GUILayout.Label(Localizer.Format("#LOC_BDArmory_PilotAI_Ramming"), BoldLabel, Width(ColumnWidth - (leftIndent * 4) - 20)); //ramming header - GUILayout.Label(Localizer.Format("#LOC_BDArmory_AIWindow_RamHelp"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20));// ramming desc + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_PilotAI_Ramming"), BoldLabel, Width(ColumnWidth - (leftIndent * 4) - 20)); //ramming header + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_RamHelp"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20));// ramming desc } if (showMisc) { - GUILayout.Label(Localizer.Format("#LOC_BDArmory_PilotAI_Misc"), BoldLabel, Width(ColumnWidth - (leftIndent * 4) - 20)); //misc header - GUILayout.Label(Localizer.Format("#LOC_BDArmory_AIWindow_miscHelp"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //misc desc - GUILayout.Label(Localizer.Format("#LOC_BDArmory_AIWindow_orbitHelp"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //orbit dir - GUILayout.Label(Localizer.Format("#LOC_BDArmory_AIWindow_standbyHelp"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //standby + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_PilotAI_Misc"), BoldLabel, Width(ColumnWidth - (leftIndent * 4) - 20)); //misc header + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_miscHelp"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //misc desc + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_orbitHelp"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //orbit dir + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_AIWindow_standbyHelp"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //standby } } EndArea(); } - scrollViewVector = GUI.BeginScrollView(new Rect(leftIndent + 100, contentTop + (entryHeight * 1.5f), (ColumnWidth * 2) - 100 - (leftIndent), WindowHeight - (entryHeight * 1.5f) - (2 * contentTop)), scrollViewVector, - new Rect(0, 0, (ColumnWidth * 2) - 120 - (leftIndent * 2), height + contentTop)); - - GUI.BeginGroup(new Rect(leftIndent, 0, (ColumnWidth * 2) - 120 - (leftIndent * 2), height), GUIContent.none, BDArmorySetup.BDGuiSkin.box); //darker box - - //GUI.Box(new Rect(0, 0, (ColumnWidth * 2) - leftIndent, height - contentTop), "", BDArmorySetup.BDGuiSkin.window); - contentWidth -= 24; - leftIndent += 3; - - if (showPID) + if (showPID || showAltitude || showSpeed || showControl || showEvade || showTerrain || showRam || showMisc) { - pidLines += 0.2f; - GUI.BeginGroup( - new Rect(0, (pidLines * entryHeight), contentWidth, pidHeight * entryHeight), - GUIContent.none, BDArmorySetup.BDGuiSkin.box); - pidLines += 0.25f; - - GUI.Label(SettinglabelRect(leftIndent, pidLines), Localizer.Format("#LOC_BDArmory_PilotAI_PID"), BoldLabel);//"Pid Controller" - pidLines++; + scrollViewVector = GUI.BeginScrollView(new Rect(leftIndent + 100, contentTop + (entryHeight * 1.5f), (ColumnWidth * 2) - 100 - (leftIndent), WindowHeight - entryHeight * 1.5f - (2 * contentTop)), scrollViewVector, new Rect(0, 0, (ColumnWidth * 2) - 120 - (leftIndent * 2), height + 5)); - if (!NumFieldsEnabled) - { - ActivePilot.steerMult = - GUI.HorizontalSlider(SettingSliderRect(leftIndent, pidLines, contentWidth), - ActivePilot.steerMult, 0.1f, ActivePilot.UpToEleven ? 200 : 20); - ActivePilot.steerMult = Mathf.Round(ActivePilot.steerMult * 10f) / 10f; - } - else - { - inputFields["steerMult"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, pidLines, contentWidth), inputFields["steerMult"].possibleValue, 6)); - ActivePilot.steerMult = (float)inputFields["steerMult"].currentValue; - } - GUI.Label(SettinglabelRect(leftIndent, pidLines), Localizer.Format("#LOC_BDArmory_SteerFactor") + ": " + ActivePilot.steerMult.ToString("0.0"), Label);//"Steer Mult" + GUI.BeginGroup(new Rect(leftIndent, 0, (ColumnWidth * 2) - 120 - (leftIndent * 2), height + 5), GUIContent.none, BDArmorySetup.BDGuiSkin.box); //darker box + contentWidth -= 24; + leftIndent += 3; - pidLines++; - if (contextTipsEnabled) - { - GUI.Label(ContextLabelRect(leftIndent, pidLines), Localizer.Format("#LOC_BDArmory_AIWindow_SteerMultLow"), Label);//"sluggish" - GUI.Label(new Rect(150 + leftIndent + (contentWidth - leftIndent - 150 - 85 - 20), (pidLines * entryHeight), 85, entryHeight), Localizer.Format("#LOC_BDArmory_AIWindow_SteerMultHi"), rightLabel);//"twitchy" - pidLines++; - } - if (!NumFieldsEnabled) - { - ActivePilot.steerKiAdjust = - GUI.HorizontalSlider(SettingSliderRect(leftIndent, pidLines, contentWidth), - ActivePilot.steerKiAdjust, 0.01f, ActivePilot.UpToEleven ? 20 : 1); - ActivePilot.steerKiAdjust = Mathf.Round(ActivePilot.steerKiAdjust * 100f) / 100f; - } - else + if (showPID) { - inputFields["steerKiAdjust"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, pidLines, contentWidth), inputFields["steerKiAdjust"].possibleValue, 6)); - ActivePilot.steerKiAdjust = (float)inputFields["steerKiAdjust"].currentValue; - } - GUI.Label(SettinglabelRect(leftIndent, pidLines), Localizer.Format("#LOC_BDArmory_SteerKi") + ": " + ActivePilot.steerKiAdjust.ToString("0.00"), Label);//"Steer Ki" - pidLines++; + pidLines += 0.2f; + GUI.BeginGroup(new Rect(0, (pidLines * entryHeight), contentWidth, pidHeight * entryHeight), GUIContent.none, BDArmorySetup.BDGuiSkin.box); + pidLines += 0.25f; - if (contextTipsEnabled) - { - GUI.Label(ContextLabelRect(leftIndent, pidLines), Localizer.Format("#LOC_BDArmory_AIWindow_SteerKiLow"), Label);//"undershoot" - GUI.Label(new Rect(150 + leftIndent + (contentWidth - leftIndent - 150 - 85 - 20), (pidLines * entryHeight), 85, entryHeight), Localizer.Format("#LOC_BDArmory_AIWindow_SteerKiHi"), rightLabel);//"Overshoot" + GUI.Label(SettinglabelRect(leftIndent, pidLines), StringUtils.Localize("#LOC_BDArmory_PilotAI_PID"), BoldLabel);//"Pid Controller" pidLines++; - } - if (!NumFieldsEnabled) - { - ActivePilot.steerDamping = - GUI.HorizontalSlider(SettingSliderRect(leftIndent, pidLines, contentWidth), - ActivePilot.steerDamping, 0.01f, ActivePilot.UpToEleven ? 100 : 8); - ActivePilot.steerDamping = Mathf.Round(ActivePilot.steerDamping * 100f) / 100f; - } - else - { - inputFields["steerDamping"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, pidLines, contentWidth), inputFields["steerDamping"].possibleValue, 6)); - ActivePilot.steerDamping = (float)inputFields["steerDamping"].currentValue; - } - GUI.Label(SettinglabelRect(leftIndent, pidLines), Localizer.Format("#LOC_BDArmory_SteerDamping") + ": " + ActivePilot.steerDamping.ToString("0.00"), Label);//"Steer Damping" - pidLines++; - if (contextTipsEnabled) - { - GUI.Label(ContextLabelRect(leftIndent, pidLines), Localizer.Format("#LOC_BDArmory_AIWindow_SteerDampLow"), Label);//"Wobbly" - GUI.Label(new Rect(150 + leftIndent + (contentWidth - leftIndent - 150 - 85 - 20), (pidLines * entryHeight), 85, entryHeight), Localizer.Format("#LOC_BDArmory_AIWindow_SteerDampHi"), rightLabel);//"Stiff" + if (!NumFieldsEnabled) + { + if (ActivePilot.steerMult != (ActivePilot.steerMult = GUI.HorizontalSlider(SettingSliderRect(leftIndent, pidLines, contentWidth), ActivePilot.steerMult, 0.1f, ActivePilot.UpToEleven ? 200 : 20))) + ActivePilot.steerMult = BDAMath.RoundToUnit(ActivePilot.steerMult, 0.1f); + } + else + { + inputFields["steerMult"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, pidLines, contentWidth), inputFields["steerMult"].possibleValue, 6, inputFieldStyle)); + ActivePilot.steerMult = (float)inputFields["steerMult"].currentValue; + } + GUI.Label(SettinglabelRect(leftIndent, pidLines), StringUtils.Localize("#LOC_BDArmory_SteerFactor") + ": " + ActivePilot.steerMult.ToString("0.0"), Label);//"Steer Mult" + + + pidLines++; + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(leftIndent, pidLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_SteerMultLow"), Label);//"sluggish" + GUI.Label(ContextLabelRectRight(leftIndent, pidLines, contentWidth), StringUtils.Localize("#LOC_BDArmory_AIWindow_SteerMultHi"), rightLabel);//"twitchy" + pidLines++; + } + if (!NumFieldsEnabled) + { + if (ActivePilot.steerKiAdjust != (ActivePilot.steerKiAdjust = GUI.HorizontalSlider(SettingSliderRect(leftIndent, pidLines, contentWidth), ActivePilot.steerKiAdjust, 0.01f, ActivePilot.UpToEleven ? 20 : 1))) + ActivePilot.steerKiAdjust = BDAMath.RoundToUnit(ActivePilot.steerKiAdjust, 0.01f); + } + else + { + inputFields["steerKiAdjust"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, pidLines, contentWidth), inputFields["steerKiAdjust"].possibleValue, 6, inputFieldStyle)); + ActivePilot.steerKiAdjust = (float)inputFields["steerKiAdjust"].currentValue; + } + GUI.Label(SettinglabelRect(leftIndent, pidLines), StringUtils.Localize("#LOC_BDArmory_SteerKi") + ": " + ActivePilot.steerKiAdjust.ToString("0.00"), Label);//"Steer Ki" pidLines++; - } - ActivePilot.dynamicSteerDamping = - GUI.Toggle(ToggleButtonRect(leftIndent, pidLines, contentWidth), - ActivePilot.dynamicSteerDamping, Localizer.Format("#LOC_BDArmory_DynamicDamping"), ActivePilot.dynamicSteerDamping ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Dynamic damping" - pidLines += 1.25f; + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(leftIndent, pidLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_SteerKiLow"), Label);//"undershoot" + GUI.Label(ContextLabelRectRight(leftIndent, pidLines, contentWidth), StringUtils.Localize("#LOC_BDArmory_AIWindow_SteerKiHi"), rightLabel);//"Overshoot" + pidLines++; + } + if (!NumFieldsEnabled) + { + if (ActivePilot.steerDamping != (ActivePilot.steerDamping = GUI.HorizontalSlider(SettingSliderRect(leftIndent, pidLines, contentWidth), ActivePilot.steerDamping, 0.1f, ActivePilot.UpToEleven ? 100 : 8))) + ActivePilot.steerDamping = BDAMath.RoundToUnit(ActivePilot.steerDamping, 0.1f); + } + else + { + inputFields["steerDamping"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, pidLines, contentWidth), inputFields["steerDamping"].possibleValue, 6, inputFieldStyle)); + ActivePilot.steerDamping = (float)inputFields["steerDamping"].currentValue; + } + GUI.Label(SettinglabelRect(leftIndent, pidLines), StringUtils.Localize("#LOC_BDArmory_SteerDamping") + ": " + ActivePilot.steerDamping.ToString("0.00"), Label);//"Steer Damping" - if (ActivePilot.dynamicSteerDamping) - { - float dynPidLines = 0; - ActivePilot.CustomDynamicAxisFields = GUI.Toggle(ToggleButtonRect(leftIndent, pidLines, contentWidth), - ActivePilot.CustomDynamicAxisFields, Localizer.Format("#LOC_BDArmory_3AxisDynamicSteerDamping"), ActivePilot.CustomDynamicAxisFields ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"3 axis damping" - dynPidLines++; - if (!ActivePilot.CustomDynamicAxisFields) + pidLines++; + if (contextTipsEnabled) { - dynPidLines += 0.25f; + GUI.Label(ContextLabelRect(leftIndent, pidLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_SteerDampLow"), Label);//"Wobbly" + GUI.Label(ContextLabelRectRight(leftIndent, pidLines, contentWidth), StringUtils.Localize("#LOC_BDArmory_AIWindow_SteerDampHi"), rightLabel);//"Stiff" + pidLines++; + } - GUI.Label(SettinglabelRect(leftIndent, pidLines + dynPidLines), Localizer.Format("#LOC_BDArmory_DynamicDamping"), BoldLabel);//"Dynamic Damping" - dynPidLines++; - if (!NumFieldsEnabled) - { - ActivePilot.DynamicDampingMin = - GUI.HorizontalSlider(SettingSliderRect(leftIndent, pidLines + dynPidLines, contentWidth), - ActivePilot.DynamicDampingMin, 1f, ActivePilot.UpToEleven ? 100 : 8); - ActivePilot.DynamicDampingMin = Mathf.Round(ActivePilot.DynamicDampingMin * 10f) / 10f; - } - else - { - inputFields["DynamicDampingMin"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, pidLines + dynPidLines, contentWidth), inputFields["DynamicDampingMin"].possibleValue, 6)); - ActivePilot.DynamicDampingMin = (float)inputFields["DynamicDampingMin"].currentValue; - } - GUI.Label(SettinglabelRect(leftIndent, pidLines + dynPidLines), Localizer.Format("#LOC_BDArmory_DynamicDampingMin") + ": " + ActivePilot.DynamicDampingMin.ToString("0.0"), Label);//"dynamic damping min" - dynPidLines++; - if (contextTipsEnabled) - { - GUI.Label(ContextLabelRect(leftIndent, pidLines + dynPidLines), Localizer.Format("#LOC_BDArmory_AIWindow_DynDampMin"), Label);//"dynamic damp min" - dynPidLines++; - } - if (!NumFieldsEnabled) - { - ActivePilot.DynamicDampingMax = - GUI.HorizontalSlider(SettingSliderRect(leftIndent, pidLines + dynPidLines, contentWidth), - ActivePilot.DynamicDampingMax, 1f, ActivePilot.UpToEleven ? 100 : 8); - ActivePilot.DynamicDampingMax = Mathf.Round(ActivePilot.DynamicDampingMax * 10f) / 10f; - } - else - { - inputFields["DynamicDampingMax"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, pidLines + dynPidLines, contentWidth), inputFields["DynamicDampingMax"].possibleValue, 6)); - ActivePilot.DynamicDampingMax = (float)inputFields["DynamicDampingMax"].currentValue; - } - GUI.Label(SettinglabelRect(leftIndent, pidLines + dynPidLines), Localizer.Format("#LOC_BDArmory_DynamicDampingMax") + ": " + ActivePilot.DynamicDampingMax.ToString("0.0"), Label);//"dynamic damping max" + ActivePilot.dynamicSteerDamping = GUI.Toggle(ToggleButtonRect(leftIndent, pidLines, contentWidth), ActivePilot.dynamicSteerDamping, StringUtils.Localize("#LOC_BDArmory_DynamicDamping"), ActivePilot.dynamicSteerDamping ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Dynamic damping" + pidLines += 1.25f; + if (ActivePilot.dynamicSteerDamping) + { + float dynPidLines = 0; + ActivePilot.CustomDynamicAxisFields = GUI.Toggle(ToggleButtonRect(leftIndent, pidLines, contentWidth), ActivePilot.CustomDynamicAxisFields, StringUtils.Localize("#LOC_BDArmory_3AxisDynamicSteerDamping"), ActivePilot.CustomDynamicAxisFields ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"3 axis damping" dynPidLines++; - if (contextTipsEnabled) - { - GUI.Label(ContextLabelRect(leftIndent, pidLines + dynPidLines), Localizer.Format("#LOC_BDArmory_AIWindow_DynDampMax"), Label);//"dynamic damp max" - dynPidLines++; - } - if (!NumFieldsEnabled) - { - ActivePilot.dynamicSteerDampingFactor = - GUI.HorizontalSlider(SettingSliderRect(leftIndent, pidLines + dynPidLines, contentWidth), - ActivePilot.dynamicSteerDampingFactor, 0.1f, ActivePilot.UpToEleven ? 100 : 10); - ActivePilot.dynamicSteerDampingFactor = Mathf.Round(ActivePilot.dynamicSteerDampingFactor * 10f) / 10f; - } - else + if (!ActivePilot.CustomDynamicAxisFields) { - inputFields["dynamicSteerDampingFactor"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, pidLines + dynPidLines, contentWidth), inputFields["dynamicSteerDampingFactor"].possibleValue, 6)); - ActivePilot.dynamicSteerDampingFactor = (float)inputFields["dynamicSteerDampingFactor"].currentValue; - } - GUI.Label(SettinglabelRect(leftIndent, pidLines + dynPidLines), Localizer.Format("#LOC_BDArmory_AIWindow_DynDampMult") + ": " + ActivePilot.dynamicSteerDampingFactor.ToString("0.0"), Label);//"dynamic damping mult" + dynPidLines += 0.25f; - dynPidLines++; - if (contextTipsEnabled) - { - GUI.Label(ContextLabelRect(leftIndent, pidLines + dynPidLines), Localizer.Format("#LOC_BDArmory_AIWindow_DynDampMult"), Label);//"dynamic damp mult" + GUI.Label(SettinglabelRect(leftIndent, pidLines + dynPidLines), StringUtils.Localize("#LOC_BDArmory_DynamicDamping") + ": " + ActivePilot.dynSteerDampingValue.ToString(), Label);//"Dynamic Damping" dynPidLines++; - } - } - else - { - ActivePilot.dynamicDampingPitch = GUI.Toggle(ToggleButtonRect(leftIndent, pidLines + dynPidLines, contentWidth), - ActivePilot.dynamicDampingPitch, Localizer.Format("#LOC_BDArmory_DynamicDampingPitch"), ActivePilot.dynamicDampingPitch ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Dynamic damp pitch" - dynPidLines += 1.25f; - - if (ActivePilot.dynamicDampingPitch) - { if (!NumFieldsEnabled) { - ActivePilot.DynamicDampingPitchMin = - GUI.HorizontalSlider(SettingSliderRect(leftIndent, pidLines + dynPidLines, contentWidth), - ActivePilot.DynamicDampingPitchMin, 1f, ActivePilot.UpToEleven ? 100 : 8); - ActivePilot.DynamicDampingPitchMin = Mathf.Round(ActivePilot.DynamicDampingPitchMin * 10f) / 10f; + if (ActivePilot.DynamicDampingMin != (ActivePilot.DynamicDampingMin = GUI.HorizontalSlider(SettingSliderRect(leftIndent, pidLines + dynPidLines, contentWidth), ActivePilot.DynamicDampingMin, 0.1f, ActivePilot.UpToEleven ? 100 : 8))) + ActivePilot.DynamicDampingMin = BDAMath.RoundToUnit(ActivePilot.DynamicDampingMin, 0.1f); } else { - inputFields["DynamicDampingPitchMin"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, pidLines + dynPidLines, contentWidth), inputFields["DynamicDampingPitchMin"].possibleValue, 6)); - ActivePilot.DynamicDampingPitchMin = (float)inputFields["DynamicDampingPitchMin"].currentValue; + inputFields["DynamicDampingMin"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, pidLines + dynPidLines, contentWidth), inputFields["DynamicDampingMin"].possibleValue, 6, inputFieldStyle)); + ActivePilot.DynamicDampingMin = (float)inputFields["DynamicDampingMin"].currentValue; } - GUI.Label(SettinglabelRect(leftIndent, pidLines + dynPidLines), Localizer.Format("#LOC_BDArmory_DynamicDampingPitchMin") + ": " + ActivePilot.DynamicDampingPitchMin.ToString("0.0"), Label);//"dynamic damping min" + GUI.Label(SettinglabelRect(leftIndent, pidLines + dynPidLines), StringUtils.Localize("#LOC_BDArmory_DynamicDampingMin") + ": " + ActivePilot.DynamicDampingMin.ToString("0.0"), Label);//"dynamic damping min" dynPidLines++; if (contextTipsEnabled) { - GUI.Label(ContextLabelRect(leftIndent, pidLines + dynPidLines), Localizer.Format("#LOC_BDArmory_AIWindow_DynDampMin"), contextLabel);//"dynamic damp min" + GUI.Label(ContextLabelRect(leftIndent, pidLines + dynPidLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_DynDampMin"), Label);//"dynamic damp min" dynPidLines++; } if (!NumFieldsEnabled) { - ActivePilot.DynamicDampingPitchMax = - GUI.HorizontalSlider(SettingSliderRect(leftIndent, pidLines + dynPidLines, contentWidth), - ActivePilot.DynamicDampingPitchMax, 1f, ActivePilot.UpToEleven ? 100 : 8); - ActivePilot.DynamicDampingPitchMax = Mathf.Round(ActivePilot.DynamicDampingPitchMax * 10f) / 10f; + if (ActivePilot.DynamicDampingMax != (ActivePilot.DynamicDampingMax = GUI.HorizontalSlider(SettingSliderRect(leftIndent, pidLines + dynPidLines, contentWidth), ActivePilot.DynamicDampingMax, 0.1f, ActivePilot.UpToEleven ? 100 : 8))) + ActivePilot.DynamicDampingMax = BDAMath.RoundToUnit(ActivePilot.DynamicDampingMax, 0.1f); } else { - inputFields["DynamicDampingPitchMax"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, pidLines + dynPidLines, contentWidth), inputFields["DynamicDampingPitchMax"].possibleValue, 6)); - ActivePilot.DynamicDampingPitchMax = (float)inputFields["DynamicDampingPitchMax"].currentValue; + inputFields["DynamicDampingMax"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, pidLines + dynPidLines, contentWidth), inputFields["DynamicDampingMax"].possibleValue, 6, inputFieldStyle)); + ActivePilot.DynamicDampingMax = (float)inputFields["DynamicDampingMax"].currentValue; } - GUI.Label(SettinglabelRect(leftIndent, pidLines + dynPidLines), Localizer.Format("#LOC_BDArmory_DynamicDampingMax") + ": " + ActivePilot.DynamicDampingPitchMax.ToString("0.0"), Label);//"dynamic damping max" + GUI.Label(SettinglabelRect(leftIndent, pidLines + dynPidLines), StringUtils.Localize("#LOC_BDArmory_DynamicDampingMax") + ": " + ActivePilot.DynamicDampingMax.ToString("0.0"), Label);//"dynamic damping max" dynPidLines++; if (contextTipsEnabled) { - GUI.Label(ContextLabelRect(leftIndent, pidLines + dynPidLines), Localizer.Format("#LOC_BDArmory_AIWindow_DynDampMax"), contextLabel);//"damp max" + GUI.Label(ContextLabelRect(leftIndent, pidLines + dynPidLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_DynDampMax"), Label);//"dynamic damp max" dynPidLines++; } if (!NumFieldsEnabled) { - ActivePilot.dynamicSteerDampingPitchFactor = - GUI.HorizontalSlider(SettingSliderRect(leftIndent, pidLines + dynPidLines, contentWidth), - ActivePilot.dynamicSteerDampingPitchFactor, 0.1f, ActivePilot.UpToEleven ? 100 : 10); - ActivePilot.dynamicSteerDampingPitchFactor = Mathf.Round(ActivePilot.dynamicSteerDampingPitchFactor * 10f) / 10f; + if (ActivePilot.dynamicSteerDampingFactor != (ActivePilot.dynamicSteerDampingFactor = GUI.HorizontalSlider(SettingSliderRect(leftIndent, pidLines + dynPidLines, contentWidth), ActivePilot.dynamicSteerDampingFactor, 0.1f, ActivePilot.UpToEleven ? 100 : 10))) + ActivePilot.dynamicSteerDampingFactor = BDAMath.RoundToUnit(ActivePilot.dynamicSteerDampingFactor, 0.1f); } else { - inputFields["dynamicSteerDampingPitchFactor"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, pidLines + dynPidLines, contentWidth), inputFields["dynamicSteerDampingPitchFactor"].possibleValue, 6)); - ActivePilot.dynamicSteerDampingPitchFactor = (float)inputFields["dynamicSteerDampingPitchFactor"].currentValue; + inputFields["dynamicSteerDampingFactor"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, pidLines + dynPidLines, contentWidth), inputFields["dynamicSteerDampingFactor"].possibleValue, 6, inputFieldStyle)); + ActivePilot.dynamicSteerDampingFactor = (float)inputFields["dynamicSteerDampingFactor"].currentValue; } - GUI.Label(SettinglabelRect(leftIndent, pidLines + dynPidLines), Localizer.Format("#LOC_BDArmory_DynamicDampingPitchFactor") + ": " + ActivePilot.dynamicSteerDampingPitchFactor.ToString("0.0"), Label);//"dynamic damping mult" + GUI.Label(SettinglabelRect(leftIndent, pidLines + dynPidLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_DynDampMult") + ": " + ActivePilot.dynamicSteerDampingFactor.ToString("0.0"), Label);//"dynamic damping mult" dynPidLines++; if (contextTipsEnabled) { - GUI.Label(ContextLabelRect(leftIndent, pidLines + dynPidLines), Localizer.Format("#LOC_BDArmory_AIWindow_DynDampMult"), contextLabel);//"dynamic damp Mult" + GUI.Label(ContextLabelRect(leftIndent, pidLines + dynPidLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_DynDampMult"), Label);//"dynamic damp mult" dynPidLines++; } } - - ActivePilot.dynamicDampingYaw = GUI.Toggle(ToggleButtonRect(leftIndent, pidLines + dynPidLines, contentWidth), - ActivePilot.dynamicDampingYaw, Localizer.Format("#LOC_BDArmory_DynamicDampingYaw"), ActivePilot.dynamicDampingYaw ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Dynamic damp yaw" - dynPidLines += 1.25f; - if (ActivePilot.dynamicDampingYaw) + else { - if (!NumFieldsEnabled) - { - ActivePilot.DynamicDampingYawMin = - GUI.HorizontalSlider(SettingSliderRect(leftIndent, pidLines + dynPidLines, contentWidth), - ActivePilot.DynamicDampingYawMin, 1f, ActivePilot.UpToEleven ? 100 : 8); - ActivePilot.DynamicDampingYawMin = Mathf.Round(ActivePilot.DynamicDampingYawMin * 10f) / 10f; - } - else - { - inputFields["DynamicDampingYawMin"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, pidLines + dynPidLines, contentWidth), inputFields["DynamicDampingYawMin"].possibleValue, 6)); - ActivePilot.DynamicDampingYawMin = (float)inputFields["DynamicDampingYawMin"].currentValue; - } - GUI.Label(SettinglabelRect(leftIndent, pidLines + dynPidLines), Localizer.Format("#LOC_BDArmory_DynamicDampingYawMin") + ": " + ActivePilot.DynamicDampingYawMin.ToString("0.0"), Label);//"dynamic damping min" + ActivePilot.dynamicDampingPitch = GUI.Toggle(ToggleButtonRect(leftIndent, pidLines + dynPidLines, contentWidth), ActivePilot.dynamicDampingPitch, StringUtils.Localize("#LOC_BDArmory_DynamicDampingPitch"), ActivePilot.dynamicDampingPitch ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Dynamic damp pitch" + dynPidLines += 1.25f; - dynPidLines++; - if (contextTipsEnabled) + if (ActivePilot.dynamicDampingPitch) { - GUI.Label(ContextLabelRect(leftIndent, pidLines + dynPidLines), Localizer.Format("#LOC_BDArmory_AIWindow_DynDampMin"), contextLabel);//"dynamic damp min" + GUI.Label(SettinglabelRect(leftIndent, pidLines + dynPidLines), StringUtils.Localize("#LOC_BDArmory_DynamicDampingPitch") + ": " + ActivePilot.dynSteerDampingPitchValue.ToString(), Label);//"dynamic damp pitch" dynPidLines++; - } - if (!NumFieldsEnabled) - { - ActivePilot.DynamicDampingYawMax = - GUI.HorizontalSlider(SettingSliderRect(leftIndent, pidLines + dynPidLines, contentWidth), - ActivePilot.DynamicDampingYawMax, 1f, ActivePilot.UpToEleven ? 100 : 8); - ActivePilot.DynamicDampingYawMax = Mathf.Round(ActivePilot.DynamicDampingYawMax * 10f) / 10f; - } - else - { - inputFields["DynamicDampingYawMax"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, pidLines + dynPidLines, contentWidth), inputFields["DynamicDampingYawMax"].possibleValue, 6)); - ActivePilot.DynamicDampingYawMax = (float)inputFields["DynamicDampingYawMax"].currentValue; - } - GUI.Label(SettinglabelRect(leftIndent, pidLines + dynPidLines), Localizer.Format("#LOC_BDArmory_DynamicDampingYawMax") + ": " + ActivePilot.DynamicDampingYawMax.ToString("0.0"), Label);//"dynamic damping max" + if (!NumFieldsEnabled) + { + if (ActivePilot.DynamicDampingPitchMin != (ActivePilot.DynamicDampingPitchMin = GUI.HorizontalSlider(SettingSliderRect(leftIndent, pidLines + dynPidLines, contentWidth), ActivePilot.DynamicDampingPitchMin, 0.1f, ActivePilot.UpToEleven ? 100 : 8))) + ActivePilot.DynamicDampingPitchMin = BDAMath.RoundToUnit(ActivePilot.DynamicDampingPitchMin, 0.1f); + } + else + { + inputFields["DynamicDampingPitchMin"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, pidLines + dynPidLines, contentWidth), inputFields["DynamicDampingPitchMin"].possibleValue, 6, inputFieldStyle)); + ActivePilot.DynamicDampingPitchMin = (float)inputFields["DynamicDampingPitchMin"].currentValue; + } + GUI.Label(SettinglabelRect(leftIndent, pidLines + dynPidLines), StringUtils.Localize("#LOC_BDArmory_DynamicDampingPitchMin") + ": " + ActivePilot.DynamicDampingPitchMin.ToString("0.0"), Label);//"dynamic damping min" + dynPidLines++; + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(leftIndent, pidLines + dynPidLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_DynDampMin"), contextLabel);//"dynamic damp min" + dynPidLines++; + } + if (!NumFieldsEnabled) + { + if (ActivePilot.DynamicDampingPitchMax != (ActivePilot.DynamicDampingPitchMax = GUI.HorizontalSlider(SettingSliderRect(leftIndent, pidLines + dynPidLines, contentWidth), ActivePilot.DynamicDampingPitchMax, 0.1f, ActivePilot.UpToEleven ? 100 : 8))) + ActivePilot.DynamicDampingPitchMax = BDAMath.RoundToUnit(ActivePilot.DynamicDampingPitchMax, 0.1f); + } + else + { + inputFields["DynamicDampingPitchMax"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, pidLines + dynPidLines, contentWidth), inputFields["DynamicDampingPitchMax"].possibleValue, 6, inputFieldStyle)); + ActivePilot.DynamicDampingPitchMax = (float)inputFields["DynamicDampingPitchMax"].currentValue; + } + GUI.Label(SettinglabelRect(leftIndent, pidLines + dynPidLines), StringUtils.Localize("#LOC_BDArmory_DynamicDampingMax") + ": " + ActivePilot.DynamicDampingPitchMax.ToString("0.0"), Label);//"dynamic damping max" - dynPidLines++; - if (contextTipsEnabled) - { - GUI.Label(ContextLabelRect(leftIndent, pidLines + dynPidLines), Localizer.Format("#LOC_BDArmory_AIWindow_DynDampMax"), contextLabel);//"dynamic damp max" dynPidLines++; - } - if (!NumFieldsEnabled) - { - ActivePilot.dynamicSteerDampingYawFactor = - GUI.HorizontalSlider(SettingSliderRect(leftIndent, pidLines + dynPidLines, contentWidth), - ActivePilot.dynamicSteerDampingYawFactor, 0.1f, ActivePilot.UpToEleven ? 100 : 10); - ActivePilot.dynamicSteerDampingYawFactor = Mathf.Round(ActivePilot.dynamicSteerDampingYawFactor * 10) / 10; - } - else - { - inputFields["dynamicSteerDampingYawFactor"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, pidLines + dynPidLines, contentWidth), inputFields["dynamicSteerDampingYawFactor"].possibleValue, 6)); - ActivePilot.dynamicSteerDampingYawFactor = (float)inputFields["dynamicSteerDampingYawFactor"].currentValue; - } - GUI.Label(SettinglabelRect(leftIndent, pidLines + dynPidLines), Localizer.Format("#LOC_BDArmory_DynamicDampingYawFactor") + ": " + ActivePilot.dynamicSteerDampingYawFactor.ToString("0.0"), Label);//"dynamic damping yaw mult" + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(leftIndent, pidLines + dynPidLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_DynDampMax"), contextLabel);//"damp max" + dynPidLines++; + } + if (!NumFieldsEnabled) + { + if (ActivePilot.dynamicSteerDampingPitchFactor != (ActivePilot.dynamicSteerDampingPitchFactor = GUI.HorizontalSlider(SettingSliderRect(leftIndent, pidLines + dynPidLines, contentWidth), ActivePilot.dynamicSteerDampingPitchFactor, 0.1f, ActivePilot.UpToEleven ? 100 : 10))) + ActivePilot.dynamicSteerDampingPitchFactor = BDAMath.RoundToUnit(ActivePilot.dynamicSteerDampingPitchFactor, 0.1f); + } + else + { + inputFields["dynamicSteerDampingPitchFactor"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, pidLines + dynPidLines, contentWidth), inputFields["dynamicSteerDampingPitchFactor"].possibleValue, 6, inputFieldStyle)); + ActivePilot.dynamicSteerDampingPitchFactor = (float)inputFields["dynamicSteerDampingPitchFactor"].currentValue; + } + GUI.Label(SettinglabelRect(leftIndent, pidLines + dynPidLines), StringUtils.Localize("#LOC_BDArmory_DynamicDampingPitchFactor") + ": " + ActivePilot.dynamicSteerDampingPitchFactor.ToString("0.0"), Label);//"dynamic damping mult" - dynPidLines++; - if (contextTipsEnabled) - { - GUI.Label(ContextLabelRect(leftIndent, pidLines + dynPidLines), Localizer.Format("#LOC_BDArmory_AIWindow_DynDampMult"), contextLabel);//"dynamic damp mult" dynPidLines++; + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(leftIndent, pidLines + dynPidLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_DynDampMult"), contextLabel);//"dynamic damp Mult" + dynPidLines++; + } } - } - ActivePilot.dynamicDampingRoll = GUI.Toggle(ToggleButtonRect(leftIndent, pidLines + dynPidLines, contentWidth), - ActivePilot.dynamicDampingRoll, Localizer.Format("#LOC_BDArmory_DynamicDampingRoll"), ActivePilot.dynamicDampingRoll ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Dynamic damp roll" - dynPidLines += 1.25f; - if (ActivePilot.dynamicDampingRoll) - { - if (!NumFieldsEnabled) - { - ActivePilot.DynamicDampingRollMin = - GUI.HorizontalSlider(SettingSliderRect(leftIndent, pidLines + dynPidLines, contentWidth), - ActivePilot.DynamicDampingRollMin, 1f, ActivePilot.UpToEleven ? 100 : 8); - ActivePilot.DynamicDampingRollMin = Mathf.Round(ActivePilot.DynamicDampingRollMin * 10f) / 10f; - } - else + ActivePilot.dynamicDampingYaw = GUI.Toggle(ToggleButtonRect(leftIndent, pidLines + dynPidLines, contentWidth), ActivePilot.dynamicDampingYaw, StringUtils.Localize("#LOC_BDArmory_DynamicDampingYaw"), ActivePilot.dynamicDampingYaw ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Dynamic damp yaw" + dynPidLines += 1.25f; + if (ActivePilot.dynamicDampingYaw) { - inputFields["DynamicDampingRollMin"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, pidLines + dynPidLines, contentWidth), inputFields["DynamicDampingRollMin"].possibleValue, 6)); - ActivePilot.DynamicDampingRollMin = (float)inputFields["DynamicDampingRollMin"].currentValue; - } - GUI.Label(SettinglabelRect(leftIndent, pidLines + dynPidLines), Localizer.Format("#LOC_BDArmory_DynamicDampingRollMin") + ": " + ActivePilot.DynamicDampingRollMin.ToString("0.0"), Label);//"dynamic damping min" + GUI.Label(SettinglabelRect(leftIndent, pidLines + dynPidLines), StringUtils.Localize("#LOC_BDArmory_DynamicDampingYaw") + ": " + ActivePilot.dynSteerDampingYawValue.ToString(), Label);//"dynamic damp yaw" + dynPidLines++; + if (!NumFieldsEnabled) + { + if (ActivePilot.DynamicDampingYawMin != (ActivePilot.DynamicDampingYawMin = GUI.HorizontalSlider(SettingSliderRect(leftIndent, pidLines + dynPidLines, contentWidth), ActivePilot.DynamicDampingYawMin, 0.1f, ActivePilot.UpToEleven ? 100 : 8))) + ActivePilot.DynamicDampingYawMin = BDAMath.RoundToUnit(ActivePilot.DynamicDampingYawMin, 0.1f); + } + else + { + inputFields["DynamicDampingYawMin"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, pidLines + dynPidLines, contentWidth), inputFields["DynamicDampingYawMin"].possibleValue, 6, inputFieldStyle)); + ActivePilot.DynamicDampingYawMin = (float)inputFields["DynamicDampingYawMin"].currentValue; + } + GUI.Label(SettinglabelRect(leftIndent, pidLines + dynPidLines), StringUtils.Localize("#LOC_BDArmory_DynamicDampingYawMin") + ": " + ActivePilot.DynamicDampingYawMin.ToString("0.0"), Label);//"dynamic damping min" - dynPidLines++; - if (contextTipsEnabled) - { - GUI.Label(ContextLabelRect(leftIndent, pidLines + dynPidLines), Localizer.Format("#LOC_BDArmory_AIWindow_DynDampMin"), contextLabel);//"dynamic damp min" dynPidLines++; - } - if (!NumFieldsEnabled) - { - ActivePilot.DynamicDampingRollMax = - GUI.HorizontalSlider(SettingSliderRect(leftIndent, pidLines + dynPidLines, contentWidth), - ActivePilot.DynamicDampingRollMax, 1f, ActivePilot.UpToEleven ? 100 : 8); - ActivePilot.DynamicDampingRollMax = Mathf.Round(ActivePilot.DynamicDampingRollMax * 10f) / 10f; - } - else - { - inputFields["DynamicDampingRollMax"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, pidLines + dynPidLines, contentWidth), inputFields["DynamicDampingRollMax"].possibleValue, 6)); - ActivePilot.DynamicDampingRollMax = (float)inputFields["DynamicDampingRollMax"].currentValue; - } - GUI.Label(SettinglabelRect(leftIndent, pidLines + dynPidLines), Localizer.Format("#LOC_BDArmory_DynamicDampingRollMax") + ": " + ActivePilot.DynamicDampingRollMax.ToString("0.0"), Label);//"dynamic damping max" + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(leftIndent, pidLines + dynPidLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_DynDampMin"), contextLabel);//"dynamic damp min" + dynPidLines++; + } + if (!NumFieldsEnabled) + { + if (ActivePilot.DynamicDampingYawMax != (ActivePilot.DynamicDampingYawMax = GUI.HorizontalSlider(SettingSliderRect(leftIndent, pidLines + dynPidLines, contentWidth), ActivePilot.DynamicDampingYawMax, 0.1f, ActivePilot.UpToEleven ? 100 : 8))) + ActivePilot.DynamicDampingYawMax = BDAMath.RoundToUnit(ActivePilot.DynamicDampingYawMax, 0.1f); + } + else + { + inputFields["DynamicDampingYawMax"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, pidLines + dynPidLines, contentWidth), inputFields["DynamicDampingYawMax"].possibleValue, 6, inputFieldStyle)); + ActivePilot.DynamicDampingYawMax = (float)inputFields["DynamicDampingYawMax"].currentValue; + } + GUI.Label(SettinglabelRect(leftIndent, pidLines + dynPidLines), StringUtils.Localize("#LOC_BDArmory_DynamicDampingYawMax") + ": " + ActivePilot.DynamicDampingYawMax.ToString("0.0"), Label);//"dynamic damping max" - dynPidLines++; - if (contextTipsEnabled) - { - GUI.Label(ContextLabelRect(leftIndent, pidLines + dynPidLines), Localizer.Format("#LOC_BDArmory_AIWindow_DynDampMax"), contextLabel);//"dynamic damp max" dynPidLines++; + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(leftIndent, pidLines + dynPidLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_DynDampMax"), contextLabel);//"dynamic damp max" + dynPidLines++; + } + if (!NumFieldsEnabled) + { + if (ActivePilot.dynamicSteerDampingYawFactor != (ActivePilot.dynamicSteerDampingYawFactor = GUI.HorizontalSlider(SettingSliderRect(leftIndent, pidLines + dynPidLines, contentWidth), ActivePilot.dynamicSteerDampingYawFactor, 0.1f, ActivePilot.UpToEleven ? 100 : 10))) + ActivePilot.dynamicSteerDampingYawFactor = BDAMath.RoundToUnit(ActivePilot.dynamicSteerDampingYawFactor, 0.1f); + } + else + { + inputFields["dynamicSteerDampingYawFactor"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, pidLines + dynPidLines, contentWidth), inputFields["dynamicSteerDampingYawFactor"].possibleValue, 6, inputFieldStyle)); + ActivePilot.dynamicSteerDampingYawFactor = (float)inputFields["dynamicSteerDampingYawFactor"].currentValue; + } + GUI.Label(SettinglabelRect(leftIndent, pidLines + dynPidLines), StringUtils.Localize("#LOC_BDArmory_DynamicDampingYawFactor") + ": " + ActivePilot.dynamicSteerDampingYawFactor.ToString("0.0"), Label);//"dynamic damping yaw mult" + + dynPidLines++; + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(leftIndent, pidLines + dynPidLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_DynDampMult"), contextLabel);//"dynamic damp mult" + dynPidLines++; + } } - if (!NumFieldsEnabled) + + ActivePilot.dynamicDampingRoll = GUI.Toggle(ToggleButtonRect(leftIndent, pidLines + dynPidLines, contentWidth), ActivePilot.dynamicDampingRoll, StringUtils.Localize("#LOC_BDArmory_DynamicDampingRoll"), ActivePilot.dynamicDampingRoll ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Dynamic damp roll" + dynPidLines += 1.25f; + if (ActivePilot.dynamicDampingRoll) { - ActivePilot.dynamicSteerDampingRollFactor = - GUI.HorizontalSlider(SettingSliderRect(leftIndent, pidLines + dynPidLines, contentWidth), - ActivePilot.dynamicSteerDampingRollFactor, 0.1f, ActivePilot.UpToEleven ? 100 : 10); - ActivePilot.dynamicSteerDampingRollFactor = Mathf.Round(ActivePilot.dynamicSteerDampingRollFactor * 10f) / 10f; + GUI.Label(SettinglabelRect(leftIndent, pidLines + dynPidLines), StringUtils.Localize("#LOC_BDArmory_DynamicDampingRoll") + ": " + ActivePilot.dynSteerDampingRollValue.ToString(), Label);//"dynamic damp roll" + dynPidLines++; + if (!NumFieldsEnabled) + { + if (ActivePilot.DynamicDampingRollMin != (ActivePilot.DynamicDampingRollMin = GUI.HorizontalSlider(SettingSliderRect(leftIndent, pidLines + dynPidLines, contentWidth), ActivePilot.DynamicDampingRollMin, 0.1f, ActivePilot.UpToEleven ? 100 : 8))) + ActivePilot.DynamicDampingRollMin = BDAMath.RoundToUnit(ActivePilot.DynamicDampingRollMin, 0.1f); + } + else + { + inputFields["DynamicDampingRollMin"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, pidLines + dynPidLines, contentWidth), inputFields["DynamicDampingRollMin"].possibleValue, 6, inputFieldStyle)); + ActivePilot.DynamicDampingRollMin = (float)inputFields["DynamicDampingRollMin"].currentValue; + } + GUI.Label(SettinglabelRect(leftIndent, pidLines + dynPidLines), StringUtils.Localize("#LOC_BDArmory_DynamicDampingRollMin") + ": " + ActivePilot.DynamicDampingRollMin.ToString("0.0"), Label);//"dynamic damping min" + + dynPidLines++; + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(leftIndent, pidLines + dynPidLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_DynDampMin"), contextLabel);//"dynamic damp min" + dynPidLines++; + } + if (!NumFieldsEnabled) + { + if (ActivePilot.DynamicDampingRollMax != (ActivePilot.DynamicDampingRollMax = GUI.HorizontalSlider(SettingSliderRect(leftIndent, pidLines + dynPidLines, contentWidth), ActivePilot.DynamicDampingRollMax, 0.1f, ActivePilot.UpToEleven ? 100 : 8))) + ActivePilot.DynamicDampingRollMax = BDAMath.RoundToUnit(ActivePilot.DynamicDampingRollMax, 0.1f); + } + else + { + inputFields["DynamicDampingRollMax"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, pidLines + dynPidLines, contentWidth), inputFields["DynamicDampingRollMax"].possibleValue, 6, inputFieldStyle)); + ActivePilot.DynamicDampingRollMax = (float)inputFields["DynamicDampingRollMax"].currentValue; + } + GUI.Label(SettinglabelRect(leftIndent, pidLines + dynPidLines), StringUtils.Localize("#LOC_BDArmory_DynamicDampingRollMax") + ": " + ActivePilot.DynamicDampingRollMax.ToString("0.0"), Label);//"dynamic damping max" + + dynPidLines++; + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(leftIndent, pidLines + dynPidLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_DynDampMax"), contextLabel);//"dynamic damp max" + dynPidLines++; + } + if (!NumFieldsEnabled) + { + if (ActivePilot.dynamicSteerDampingRollFactor != (ActivePilot.dynamicSteerDampingRollFactor = GUI.HorizontalSlider(SettingSliderRect(leftIndent, pidLines + dynPidLines, contentWidth), ActivePilot.dynamicSteerDampingRollFactor, 0.1f, ActivePilot.UpToEleven ? 100 : 10))) + ActivePilot.dynamicSteerDampingRollFactor = BDAMath.RoundToUnit(ActivePilot.dynamicSteerDampingRollFactor, 0.1f); + } + else + { + inputFields["dynamicSteerDampingRollFactor"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, pidLines + dynPidLines, contentWidth), inputFields["dynamicSteerDampingRollFactor"].possibleValue, 6, inputFieldStyle)); + ActivePilot.dynamicSteerDampingRollFactor = (float)inputFields["dynamicSteerDampingRollFactor"].currentValue; + } + GUI.Label(SettinglabelRect(leftIndent, pidLines + dynPidLines), StringUtils.Localize("#LOC_BDArmory_DynamicDampingRollFactor") + ": " + ActivePilot.dynamicSteerDampingRollFactor.ToString("0.0"), Label);//"dynamic damping roll mult" + dynPidLines++; + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(leftIndent, pidLines + dynPidLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_DynDampMult"), contextLabel);//"dynamic damp mult" + dynPidLines++; + } } - else + } + pidLines += dynPidLines; + } + + #region AutoTune + if (ActivePilot.AutoTune != GUI.Toggle(ToggleButtonRect(leftIndent, pidLines, contentWidth), ActivePilot.AutoTune, StringUtils.Localize("#LOC_BDArmory_PIDAutoTune"), ActivePilot.AutoTune ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button)) + { + ActivePilot.AutoTune = !ActivePilot.AutoTune; // Only actually toggle it when needed as the setter does extra stuff. + } + pidLines += 1.25f; + if (ActivePilot.AutoTune) // Auto-tuning + { + float autoTuneLines = 0.25f; + GUI.Label(SettinglabelRect(leftIndent, pidLines + autoTuneLines++), StringUtils.Localize("#LOC_BDArmory_AutoTuningLoss") + $": {ActivePilot.autoTuningLossLabel}", Label); + GUI.Label(SettinglabelRect(leftIndent, pidLines + autoTuneLines++), $"\tParams: {ActivePilot.autoTuningLossLabel2}", Label); + GUI.Label(SettinglabelRect(leftIndent, pidLines + autoTuneLines++), $"\tField: {ActivePilot.autoTuningLossLabel3}", Label); + + if (!NumFieldsEnabled) ActivePilot.autoTuningOptionNumSamples = BDAMath.RoundToUnit(GUI.HorizontalSlider(SettingSliderRect(leftIndent, pidLines + autoTuneLines, contentWidth), ActivePilot.autoTuningOptionNumSamples, 1f, 10f), 1f); + else + { + inputFields["autoTuningOptionNumSamples"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, pidLines + autoTuneLines, contentWidth), inputFields["autoTuningOptionNumSamples"].possibleValue, 6, inputFieldStyle)); + ActivePilot.autoTuningOptionNumSamples = (float)inputFields["autoTuningOptionNumSamples"].currentValue; + } + GUI.Label(SettinglabelRect(leftIndent, pidLines + autoTuneLines++), StringUtils.Localize("#LOC_BDArmory_AIWindow_PIDAutoTuningNumSamples") + $": {ActivePilot.autoTuningOptionNumSamples}", Label); + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(leftIndent, pidLines + autoTuneLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_PIDAutoTuningNumSamplesMin"), Label); + GUI.Label(ContextLabelRectRight(leftIndent, pidLines + autoTuneLines, contentWidth), StringUtils.Localize("#LOC_BDArmory_AIWindow_PIDAutoTuningNumSamplesMax"), rightLabel); + ++autoTuneLines; + } + + if (!NumFieldsEnabled) ActivePilot.autoTuningOptionFastResponseRelevance = BDAMath.RoundToUnit(GUI.HorizontalSlider(SettingSliderRect(leftIndent, pidLines + autoTuneLines, contentWidth), ActivePilot.autoTuningOptionFastResponseRelevance, 0f, 0.5f), 0.01f); + else + { + inputFields["autoTuningOptionFastResponseRelevance"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, pidLines + autoTuneLines, contentWidth), inputFields["autoTuningOptionFastResponseRelevance"].possibleValue, 6, inputFieldStyle)); + ActivePilot.autoTuningOptionFastResponseRelevance = (float)inputFields["autoTuningOptionFastResponseRelevance"].currentValue; + } + GUI.Label(SettinglabelRect(leftIndent, pidLines + autoTuneLines++), StringUtils.Localize("#LOC_BDArmory_AIWindow_PIDAutoTuningFastResponseRelevance") + $": {ActivePilot.autoTuningOptionFastResponseRelevance:G3}", Label); + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(leftIndent, pidLines + autoTuneLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_PIDAutoTuningFastResponseRelevanceMin"), Label); + GUI.Label(ContextLabelRectRight(leftIndent, pidLines + autoTuneLines, contentWidth), StringUtils.Localize("#LOC_BDArmory_AIWindow_PIDAutoTuningFastResponseRelevanceMax"), rightLabel); + ++autoTuneLines; + } + + if (!NumFieldsEnabled) ActivePilot.autoTuningOptionInitialLearningRate = Mathf.Pow(10f, BDAMath.RoundToUnit(GUI.HorizontalSlider(SettingSliderRect(leftIndent, pidLines + autoTuneLines, contentWidth), Mathf.Log10(ActivePilot.autoTuningOptionInitialLearningRate), -3f, 0f), 0.5f)); + else + { + inputFields["autoTuningOptionInitialLearningRate"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, pidLines + autoTuneLines, contentWidth), inputFields["autoTuningOptionInitialLearningRate"].possibleValue, 7, inputFieldStyle)); + ActivePilot.autoTuningOptionInitialLearningRate = (float)inputFields["autoTuningOptionInitialLearningRate"].currentValue; + } + GUI.Label(SettinglabelRect(leftIndent, pidLines + autoTuneLines++), StringUtils.Localize("#LOC_BDArmory_AIWindow_PIDAutoTuningInitialLearningRate") + $": {ActivePilot.autoTuningOptionInitialLearningRate:G3}", Label); + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(leftIndent, pidLines + autoTuneLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_PIDAutoTuningInitialLearningRateContext"), Label); + ++autoTuneLines; + } + + if (!NumFieldsEnabled) ActivePilot.autoTuningAltitude = BDAMath.RoundToUnit(GUI.HorizontalSlider(SettingSliderRect(leftIndent, pidLines + autoTuneLines, contentWidth), ActivePilot.autoTuningAltitude, 50f, 5000f), 50f); + else + { + inputFields["autoTuningAltitude"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, pidLines + autoTuneLines, contentWidth), inputFields["autoTuningAltitude"].possibleValue, 6, inputFieldStyle)); + ActivePilot.autoTuningAltitude = (float)inputFields["autoTuningAltitude"].currentValue; + } + GUI.Label(SettinglabelRect(leftIndent, pidLines + autoTuneLines++), StringUtils.Localize("#LOC_BDArmory_AIWindow_PIDAutoTuningAltitude") + $": {ActivePilot.autoTuningAltitude}", Label); + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(leftIndent, pidLines + autoTuneLines++), StringUtils.Localize("#LOC_BDArmory_AIWindow_PIDAutoTuningAltitudeContext"), Label); + } + + if (!NumFieldsEnabled) ActivePilot.autoTuningSpeed = BDAMath.RoundToUnit(GUI.HorizontalSlider(SettingSliderRect(leftIndent, pidLines + autoTuneLines, contentWidth), ActivePilot.autoTuningSpeed, 50f, 800f), 5f); + else + { + inputFields["autoTuningSpeed"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, pidLines + autoTuneLines, contentWidth), inputFields["autoTuningSpeed"].possibleValue, 6, inputFieldStyle)); + ActivePilot.autoTuningSpeed = (float)inputFields["autoTuningSpeed"].currentValue; + } + GUI.Label(SettinglabelRect(leftIndent, pidLines + autoTuneLines++), StringUtils.Localize("#LOC_BDArmory_AIWindow_PIDAutoTuningSpeed") + $": {ActivePilot.autoTuningSpeed}", Label); + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(leftIndent, pidLines + autoTuneLines++), StringUtils.Localize("#LOC_BDArmory_AIWindow_PIDAutoTuningSpeedContext"), Label); + } + + fixedAutoTuneFields = GUI.Toggle(ToggleButtonRects(leftIndent, pidLines + autoTuneLines, 0, 2, contentWidth), fixedAutoTuneFields, StringUtils.Localize("#LOC_BDArmory_AIWindow_PIDAutoTuningFixedFields"), fixedAutoTuneFields ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button); + + ActivePilot.autoTuningOptionClampMaximums = GUI.Toggle(ToggleButtonRects(leftIndent, pidLines + autoTuneLines, 1, 2, contentWidth), ActivePilot.autoTuningOptionClampMaximums, StringUtils.Localize("#LOC_BDArmory_AIWindow_PIDAutoTuningClampMaximums"), ActivePilot.autoTuningOptionClampMaximums ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button); + ++autoTuneLines; + + if (fixedAutoTuneFields) + { + bool resetGradient = false; + if (!ActivePilot.dynamicSteerDamping) { - inputFields["dynamicSteerDampingRollFactor"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, pidLines + dynPidLines, contentWidth), inputFields["dynamicSteerDampingRollFactor"].possibleValue, 6)); - ActivePilot.dynamicSteerDampingRollFactor = (float)inputFields["dynamicSteerDampingRollFactor"].currentValue; + if (ActivePilot.autoTuningOptionFixedP != (ActivePilot.autoTuningOptionFixedP = GUI.Toggle(ToggleButtonRects(leftIndent, pidLines + autoTuneLines, 0, 3, contentWidth), ActivePilot.autoTuningOptionFixedP, StringUtils.Localize("P"), ActivePilot.autoTuningOptionFixedP ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) resetGradient = true; + if (ActivePilot.autoTuningOptionFixedI != (ActivePilot.autoTuningOptionFixedI = GUI.Toggle(ToggleButtonRects(leftIndent, pidLines + autoTuneLines, 1, 3, contentWidth), ActivePilot.autoTuningOptionFixedI, StringUtils.Localize("I"), ActivePilot.autoTuningOptionFixedI ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) resetGradient = true; + if (ActivePilot.autoTuningOptionFixedD != (ActivePilot.autoTuningOptionFixedD = GUI.Toggle(ToggleButtonRects(leftIndent, pidLines + autoTuneLines, 2, 3, contentWidth), ActivePilot.autoTuningOptionFixedD, StringUtils.Localize("D"), ActivePilot.autoTuningOptionFixedD ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) resetGradient = true; } - GUI.Label(SettinglabelRect(leftIndent, pidLines + dynPidLines), Localizer.Format("#LOC_BDArmory_DynamicDampingRollFactor") + ": " + ActivePilot.dynamicSteerDampingRollFactor.ToString("0.0"), Label);//"dynamic damping roll mult" - dynPidLines++; - if (contextTipsEnabled) + else if (!ActivePilot.CustomDynamicAxisFields) { - GUI.Label(ContextLabelRect(leftIndent, pidLines + dynPidLines), Localizer.Format("#LOC_BDArmory_AIWindow_DynDampMult"), contextLabel);//"dynamic damp mult" - dynPidLines++; + if (ActivePilot.autoTuningOptionFixedP != (ActivePilot.autoTuningOptionFixedP = GUI.Toggle(ToggleButtonRects(leftIndent, pidLines + autoTuneLines, 0, 5, contentWidth), ActivePilot.autoTuningOptionFixedP, StringUtils.Localize("P"), ActivePilot.autoTuningOptionFixedP ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) resetGradient = true; + if (ActivePilot.autoTuningOptionFixedI != (ActivePilot.autoTuningOptionFixedI = GUI.Toggle(ToggleButtonRects(leftIndent, pidLines + autoTuneLines, 1, 5, contentWidth), ActivePilot.autoTuningOptionFixedI, StringUtils.Localize("I"), ActivePilot.autoTuningOptionFixedI ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) resetGradient = true; + if (ActivePilot.autoTuningOptionFixedDOff != (ActivePilot.autoTuningOptionFixedDOff = GUI.Toggle(ToggleButtonRects(leftIndent, pidLines + autoTuneLines, 2, 5, contentWidth), ActivePilot.autoTuningOptionFixedDOff, StringUtils.Localize("DOff"), ActivePilot.autoTuningOptionFixedDOff ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) resetGradient = true; + if (ActivePilot.autoTuningOptionFixedDOn != (ActivePilot.autoTuningOptionFixedDOn = GUI.Toggle(ToggleButtonRects(leftIndent, pidLines + autoTuneLines, 3, 5, contentWidth), ActivePilot.autoTuningOptionFixedDOn, StringUtils.Localize("DOn"), ActivePilot.autoTuningOptionFixedDOn ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) resetGradient = true; + if (ActivePilot.autoTuningOptionFixedDF != (ActivePilot.autoTuningOptionFixedDF = GUI.Toggle(ToggleButtonRects(leftIndent, pidLines + autoTuneLines, 4, 5, contentWidth), ActivePilot.autoTuningOptionFixedDF, StringUtils.Localize("DF"), ActivePilot.autoTuningOptionFixedDF ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) resetGradient = true; + } + else + { + if (ActivePilot.autoTuningOptionFixedP != (ActivePilot.autoTuningOptionFixedP = GUI.Toggle(ToggleButtonRects(leftIndent, pidLines + autoTuneLines, 0, 11, contentWidth), ActivePilot.autoTuningOptionFixedP, StringUtils.Localize("P"), ActivePilot.autoTuningOptionFixedP ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) resetGradient = true; + if (ActivePilot.autoTuningOptionFixedI != (ActivePilot.autoTuningOptionFixedI = GUI.Toggle(ToggleButtonRects(leftIndent, pidLines + autoTuneLines, 1, 11, contentWidth), ActivePilot.autoTuningOptionFixedI, StringUtils.Localize("I"), ActivePilot.autoTuningOptionFixedI ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) resetGradient = true; + if (ActivePilot.autoTuningOptionFixedDPOff != (ActivePilot.autoTuningOptionFixedDPOff = GUI.Toggle(ToggleButtonRects(leftIndent, pidLines + autoTuneLines, 2, 11, contentWidth), ActivePilot.autoTuningOptionFixedDPOff, StringUtils.Localize("DPOff"), ActivePilot.autoTuningOptionFixedDPOff ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) resetGradient = true; + if (ActivePilot.autoTuningOptionFixedDPOn != (ActivePilot.autoTuningOptionFixedDPOn = GUI.Toggle(ToggleButtonRects(leftIndent, pidLines + autoTuneLines, 3, 11, contentWidth), ActivePilot.autoTuningOptionFixedDPOn, StringUtils.Localize("DPOn"), ActivePilot.autoTuningOptionFixedDPOn ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) resetGradient = true; + if (ActivePilot.autoTuningOptionFixedDPF != (ActivePilot.autoTuningOptionFixedDPF = GUI.Toggle(ToggleButtonRects(leftIndent, pidLines + autoTuneLines, 4, 11, contentWidth), ActivePilot.autoTuningOptionFixedDPF, StringUtils.Localize("DPF"), ActivePilot.autoTuningOptionFixedDPF ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) resetGradient = true; + if (ActivePilot.autoTuningOptionFixedDYOff != (ActivePilot.autoTuningOptionFixedDYOff = GUI.Toggle(ToggleButtonRects(leftIndent, pidLines + autoTuneLines, 5, 11, contentWidth), ActivePilot.autoTuningOptionFixedDYOff, StringUtils.Localize("DYOff"), ActivePilot.autoTuningOptionFixedDYOff ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) resetGradient = true; + if (ActivePilot.autoTuningOptionFixedDYOn != (ActivePilot.autoTuningOptionFixedDYOn = GUI.Toggle(ToggleButtonRects(leftIndent, pidLines + autoTuneLines, 6, 11, contentWidth), ActivePilot.autoTuningOptionFixedDYOn, StringUtils.Localize("DYOn"), ActivePilot.autoTuningOptionFixedDYOn ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) resetGradient = true; + if (ActivePilot.autoTuningOptionFixedDYF != (ActivePilot.autoTuningOptionFixedDYF = GUI.Toggle(ToggleButtonRects(leftIndent, pidLines + autoTuneLines, 7, 11, contentWidth), ActivePilot.autoTuningOptionFixedDYF, StringUtils.Localize("DYF"), ActivePilot.autoTuningOptionFixedDYF ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) resetGradient = true; + if (ActivePilot.autoTuningOptionFixedDROff != (ActivePilot.autoTuningOptionFixedDROff = GUI.Toggle(ToggleButtonRects(leftIndent, pidLines + autoTuneLines, 8, 11, contentWidth), ActivePilot.autoTuningOptionFixedDROff, StringUtils.Localize("DROff"), ActivePilot.autoTuningOptionFixedDROff ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) resetGradient = true; + if (ActivePilot.autoTuningOptionFixedDROn != (ActivePilot.autoTuningOptionFixedDROn = GUI.Toggle(ToggleButtonRects(leftIndent, pidLines + autoTuneLines, 9, 11, contentWidth), ActivePilot.autoTuningOptionFixedDROn, StringUtils.Localize("DROn"), ActivePilot.autoTuningOptionFixedDROn ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) resetGradient = true; + if (ActivePilot.autoTuningOptionFixedDRF != (ActivePilot.autoTuningOptionFixedDRF = GUI.Toggle(ToggleButtonRects(leftIndent, pidLines + autoTuneLines, 10, 11, contentWidth), ActivePilot.autoTuningOptionFixedDRF, StringUtils.Localize("DRF"), ActivePilot.autoTuningOptionFixedDRF ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))) resetGradient = true; } + if (resetGradient && HighLogic.LoadedSceneIsFlight) ActivePilot.pidAutoTuning.ResetGradient(); + ++autoTuneLines; } + + pidLines += autoTuneLines + 0.25f; } - pidLines += dynPidLines; - pidLines += 1.25f; - } - GUI.EndGroup(); - pidHeight = Mathf.Lerp(pidHeight, pidLines, 0.15f); - pidLines += 0.1f; + #endregion - } + GUI.EndGroup(); + pidHeight = Mathf.Lerp(pidHeight, pidLines, 0.15f); + pidLines += 0.1f; - if (showAltitude) - { - altLines += 0.2f; - GUI.BeginGroup( - new Rect(0, ((pidLines + altLines) * entryHeight), contentWidth, altitudeHeight * entryHeight), - GUIContent.none, BDArmorySetup.BDGuiSkin.box); - altLines += 0.25f; - - GUI.Label(SettinglabelRect(leftIndent, altLines), Localizer.Format("#LOC_BDArmory_PilotAI_Altitudes"), BoldLabel);//"Altitudes" - altLines++; - var oldDefaultAlt = ActivePilot.defaultAltitude; - if (!NumFieldsEnabled) - { - ActivePilot.defaultAltitude = - GUI.HorizontalSlider(SettingSliderRect(leftIndent, altLines, contentWidth), - ActivePilot.defaultAltitude, 100, ActivePilot.UpToEleven ? 100000 : 15000); - ActivePilot.defaultAltitude = Mathf.Round(ActivePilot.defaultAltitude / 25) * 25; - } - else - { - inputFields["defaultAltitude"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, altLines, contentWidth), inputFields["defaultAltitude"].possibleValue, 6)); - ActivePilot.defaultAltitude = (float)inputFields["defaultAltitude"].currentValue; - } - if (ActivePilot.defaultAltitude != oldDefaultAlt) - { - ActivePilot.ClampAltitudes("defaultAltitude"); - inputFields["minAltitude"].currentValue = ActivePilot.minAltitude; - inputFields["maxAltitude"].currentValue = ActivePilot.maxAltitude; - } - GUI.Label(SettinglabelRect(leftIndent, altLines), Localizer.Format("#LOC_BDArmory_DefaultAltitude") + ": " + ActivePilot.defaultAltitude.ToString("0"), Label);//"default altitude" - altLines++; - if (contextTipsEnabled) - { - GUI.Label(ContextLabelRect(leftIndent, altLines), Localizer.Format("#LOC_BDArmory_AIWindow_DefAlt"), contextLabel);//"defalult alt" - altLines++; - } - var oldMinAlt = ActivePilot.minAltitude; - if (!NumFieldsEnabled) - { - ActivePilot.minAltitude = - GUI.HorizontalSlider(SettingSliderRect(leftIndent, altLines, contentWidth), - ActivePilot.minAltitude, 25, ActivePilot.UpToEleven ? 60000 : 6000); - ActivePilot.minAltitude = Mathf.Round(ActivePilot.minAltitude / 25) * 25; - } - else - { - inputFields["minAltitude"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, altLines, contentWidth), inputFields["minAltitude"].possibleValue, 6)); - ActivePilot.minAltitude = (float)inputFields["minAltitude"].currentValue; - } - if (ActivePilot.minAltitude != oldMinAlt) - { - ActivePilot.ClampAltitudes("minAltitude"); - inputFields["defaultAltitude"].currentValue = ActivePilot.defaultAltitude; - inputFields["maxAltitude"].currentValue = ActivePilot.maxAltitude; - } - GUI.Label(SettinglabelRect(leftIndent, altLines), Localizer.Format("#LOC_BDArmory_MinAltitude") + ": " + ActivePilot.minAltitude.ToString("0"), Label);//"min altitude" - altLines++; - if (contextTipsEnabled) - { - GUI.Label(ContextLabelRect(leftIndent, altLines), Localizer.Format("#LOC_BDArmory_AIWindow_MinAlt"), contextLabel);//"min alt" - altLines++; } - ActivePilot.maxAltitudeToggle = GUI.Toggle(new Rect(leftIndent, altLines * entryHeight, contentWidth - (2 * leftIndent), entryHeight), - ActivePilot.maxAltitudeToggle, Localizer.Format("#LOC_BDArmory_MaxAltitude"), ActivePilot.maxAltitudeToggle ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"max altitude AGL" - altLines += 1.25f; - - if (ActivePilot.maxAltitudeToggle) + if (showAltitude) { - var oldMaxAlt = ActivePilot.maxAltitude; + altLines += 0.2f; + GUI.BeginGroup( + new Rect(0, ((pidLines + altLines) * entryHeight), contentWidth, altitudeHeight * entryHeight), + GUIContent.none, BDArmorySetup.BDGuiSkin.box); + altLines += 0.25f; + + GUI.Label(SettinglabelRect(leftIndent, altLines), StringUtils.Localize("#LOC_BDArmory_PilotAI_Altitudes"), BoldLabel);//"Altitudes" + altLines++; + var oldDefaultAlt = ActivePilot.defaultAltitude; if (!NumFieldsEnabled) { - ActivePilot.maxAltitude = + ActivePilot.defaultAltitude = GUI.HorizontalSlider(SettingSliderRect(leftIndent, altLines, contentWidth), - ActivePilot.maxAltitude, 100, ActivePilot.UpToEleven ? 100000 : 15000); - ActivePilot.maxAltitude = Mathf.Round(ActivePilot.maxAltitude / 25) * 25; + ActivePilot.defaultAltitude, 100, ActivePilot.UpToEleven ? 100000 : 15000); + ActivePilot.defaultAltitude = Mathf.Round(ActivePilot.defaultAltitude / 50) * 50; } else { - inputFields["maxAltitude"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, altLines, contentWidth), inputFields["maxAltitude"].possibleValue, 6)); - ActivePilot.maxAltitude = (float)inputFields["maxAltitude"].currentValue; + inputFields["defaultAltitude"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, altLines, contentWidth), inputFields["defaultAltitude"].possibleValue, 6, inputFieldStyle)); + ActivePilot.defaultAltitude = (float)inputFields["defaultAltitude"].currentValue; } - if (ActivePilot.maxAltitude != oldMaxAlt) + if (ActivePilot.defaultAltitude != oldDefaultAlt) { - ActivePilot.ClampAltitudes("maxAltitude"); + ActivePilot.ClampFields("defaultAltitude"); inputFields["minAltitude"].currentValue = ActivePilot.minAltitude; + inputFields["maxAltitude"].currentValue = ActivePilot.maxAltitude; + } + GUI.Label(SettinglabelRect(leftIndent, altLines), StringUtils.Localize("#LOC_BDArmory_DefaultAltitude") + ": " + ActivePilot.defaultAltitude.ToString("0"), Label);//"default altitude" + altLines++; + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(leftIndent, altLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_DefAlt"), contextLabel);//"defalult alt" + altLines++; + } + var oldMinAlt = ActivePilot.minAltitude; + if (!NumFieldsEnabled) + { + ActivePilot.minAltitude = + GUI.HorizontalSlider(SettingSliderRect(leftIndent, altLines, contentWidth), + ActivePilot.minAltitude, 25, ActivePilot.UpToEleven ? 60000 : 6000); + ActivePilot.minAltitude = Mathf.Round(ActivePilot.minAltitude / 10) * 10; + } + else + { + inputFields["minAltitude"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, altLines, contentWidth), inputFields["minAltitude"].possibleValue, 6, inputFieldStyle)); + ActivePilot.minAltitude = (float)inputFields["minAltitude"].currentValue; + } + if (ActivePilot.minAltitude != oldMinAlt) + { + ActivePilot.ClampFields("minAltitude"); inputFields["defaultAltitude"].currentValue = ActivePilot.defaultAltitude; + inputFields["maxAltitude"].currentValue = ActivePilot.maxAltitude; } - GUI.Label(SettinglabelRect(leftIndent, altLines), Localizer.Format("#LOC_BDArmory_MaxAltitude") + ": " + ActivePilot.maxAltitude.ToString("0"), Label);//"max altitude" + GUI.Label(SettinglabelRect(leftIndent, altLines), StringUtils.Localize("#LOC_BDArmory_MinAltitude") + ": " + ActivePilot.minAltitude.ToString("0"), Label);//"min altitude" altLines++; if (contextTipsEnabled) { - GUI.Label(ContextLabelRect(leftIndent, altLines), Localizer.Format("#LOC_BDArmory_AIWindow_MaxAlt"), contextLabel);//"max alt" + GUI.Label(ContextLabelRect(leftIndent, altLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_MinAlt"), contextLabel);//"min alt" altLines++; } - } - GUI.EndGroup(); - altitudeHeight = Mathf.Lerp(altitudeHeight, altLines, 0.15f); - altLines += 0.1f; - } - - if (showSpeed) - { - spdLines += 0.2f; - GUI.BeginGroup( - new Rect(0, ((pidLines + altLines + spdLines) * entryHeight), contentWidth, speedHeight * entryHeight), - GUIContent.none, BDArmorySetup.BDGuiSkin.box); - spdLines += 0.25f; - - GUI.Label(SettinglabelRect(leftIndent, spdLines), Localizer.Format("#LOC_BDArmory_PilotAI_Speeds"), BoldLabel);//"Speed" - if (!NumFieldsEnabled) - { - ActivePilot.maxSpeed = GUI.HorizontalSlider(SettingSliderRect(leftIndent, ++spdLines, contentWidth), ActivePilot.maxSpeed, 20, ActivePilot.UpToEleven ? 3000 : 800); - ActivePilot.maxSpeed = Mathf.Round(ActivePilot.maxSpeed); - } - else - { - inputFields["maxSpeed"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, ++spdLines, contentWidth), inputFields["maxSpeed"].possibleValue, 6)); - ActivePilot.maxSpeed = (float)inputFields["maxSpeed"].currentValue; - } - GUI.Label(SettinglabelRect(leftIndent, spdLines), Localizer.Format("#LOC_BDArmory_MaxSpeed") + ": " + ActivePilot.maxSpeed.ToString("0"), Label);//"max speed" - if (contextTipsEnabled) GUI.Label(ContextLabelRect(leftIndent, ++spdLines), Localizer.Format("#LOC_BDArmory_AIWindow_maxSpeed"), contextLabel);//"max speed" - - if (!NumFieldsEnabled) - { - ActivePilot.takeOffSpeed = GUI.HorizontalSlider(SettingSliderRect(leftIndent, ++spdLines, contentWidth), ActivePilot.takeOffSpeed, 10f, ActivePilot.UpToEleven ? 2000 : 200); - ActivePilot.takeOffSpeed = Mathf.Round(ActivePilot.takeOffSpeed); - } - else - { - inputFields["takeOffSpeed"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, ++spdLines, contentWidth), inputFields["takeOffSpeed"].possibleValue, 6)); - ActivePilot.takeOffSpeed = (float)inputFields["takeOffSpeed"].currentValue; - } - GUI.Label(SettinglabelRect(leftIndent, spdLines), Localizer.Format("#LOC_BDArmory_TakeOffSpeed") + ": " + ActivePilot.takeOffSpeed.ToString("0"), Label);//"takeoff speed" - if (contextTipsEnabled) GUI.Label(ContextLabelRect(leftIndent, ++spdLines), Localizer.Format("#LOC_BDArmory_AIWindow_takeoff"), contextLabel);//"takeoff speed help" - - if (!NumFieldsEnabled) - { - ActivePilot.minSpeed = GUI.HorizontalSlider(SettingSliderRect(leftIndent, ++spdLines, contentWidth), ActivePilot.minSpeed, 10, ActivePilot.UpToEleven ? 2000 : 200); - ActivePilot.minSpeed = Mathf.Round(ActivePilot.minSpeed); - } - else - { - inputFields["minSpeed"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, ++spdLines, contentWidth), inputFields["minSpeed"].possibleValue, 6)); - ActivePilot.minSpeed = (float)inputFields["minSpeed"].currentValue; - } - GUI.Label(SettinglabelRect(leftIndent, spdLines), Localizer.Format("#LOC_BDArmory_MinSpeed") + ": " + ActivePilot.minSpeed.ToString("0"), Label);//"min speed" - if (contextTipsEnabled) GUI.Label(ContextLabelRect(leftIndent, ++spdLines), Localizer.Format("#LOC_BDArmory_AIWindow_minSpeed"), contextLabel);//"min speed help" - - if (!NumFieldsEnabled) - { - ActivePilot.strafingSpeed = GUI.HorizontalSlider(SettingSliderRect(leftIndent, ++spdLines, contentWidth), ActivePilot.strafingSpeed, 10, 200); - ActivePilot.strafingSpeed = Mathf.Round(ActivePilot.strafingSpeed); - } - else - { - inputFields["strafingSpeed"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, ++spdLines, contentWidth), inputFields["strafingSpeed"].possibleValue, 6)); - ActivePilot.strafingSpeed = (float)inputFields["strafingSpeed"].currentValue; - } - GUI.Label(SettinglabelRect(leftIndent, spdLines), Localizer.Format("#LOC_BDArmory_StrafingSpeed") + ": " + ActivePilot.strafingSpeed.ToString("0"), Label);//"strafing speed" - if (contextTipsEnabled) GUI.Label(ContextLabelRect(leftIndent, ++spdLines), Localizer.Format("#LOC_BDArmory_AIWindow_atkSpeed"), contextLabel);//"strafe speed" - if (!NumFieldsEnabled) - { - ActivePilot.idleSpeed = GUI.HorizontalSlider(SettingSliderRect(leftIndent, ++spdLines, contentWidth), ActivePilot.idleSpeed, 10, ActivePilot.UpToEleven ? 3000 : 200); - ActivePilot.idleSpeed = Mathf.Round(ActivePilot.idleSpeed); - } - else - { - inputFields["idleSpeed"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, ++spdLines, contentWidth), inputFields["idleSpeed"].possibleValue, 6)); - ActivePilot.idleSpeed = (float)inputFields["idleSpeed"].currentValue; - } - GUI.Label(SettinglabelRect(leftIndent, spdLines), Localizer.Format("#LOC_BDArmory_IdleSpeed") + ": " + ActivePilot.idleSpeed.ToString("0"), Label);//"idle speed" - if (contextTipsEnabled) GUI.Label(ContextLabelRect(leftIndent, ++spdLines), Localizer.Format("#LOC_BDArmory_AIWindow_idleSpeed"), contextLabel);//"idle speed context help" + ActivePilot.maxAltitudeToggle = GUI.Toggle(new Rect(leftIndent, altLines * entryHeight, contentWidth - (2 * leftIndent), entryHeight), + ActivePilot.maxAltitudeToggle, StringUtils.Localize("#LOC_BDArmory_MaxAltitude"), ActivePilot.maxAltitudeToggle ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"max altitude AGL" + altLines += 1.25f; - if (!NumFieldsEnabled) - { - ActivePilot.ABPriority = GUI.HorizontalSlider(SettingSliderRect(leftIndent, ++spdLines, contentWidth), ActivePilot.ABPriority, 0, 100); - ActivePilot.ABPriority = Mathf.Round(ActivePilot.ABPriority); - } - else - { - inputFields["ABPriority"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, ++spdLines, contentWidth), inputFields["ABPriority"].possibleValue, 6)); - ActivePilot.ABPriority = (float)inputFields["ABPriority"].currentValue; + if (ActivePilot.maxAltitudeToggle) + { + var oldMaxAlt = ActivePilot.maxAltitude; + if (!NumFieldsEnabled) + { + ActivePilot.maxAltitude = + GUI.HorizontalSlider(SettingSliderRect(leftIndent, altLines, contentWidth), + ActivePilot.maxAltitude, 100, ActivePilot.UpToEleven ? 100000 : 15000); + ActivePilot.maxAltitude = Mathf.Round(ActivePilot.maxAltitude / 100) * 100; + } + else + { + inputFields["maxAltitude"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, altLines, contentWidth), inputFields["maxAltitude"].possibleValue, 6, inputFieldStyle)); + ActivePilot.maxAltitude = (float)inputFields["maxAltitude"].currentValue; + } + if (ActivePilot.maxAltitude != oldMaxAlt) + { + ActivePilot.ClampFields("maxAltitude"); + inputFields["minAltitude"].currentValue = ActivePilot.minAltitude; + inputFields["defaultAltitude"].currentValue = ActivePilot.defaultAltitude; + } + GUI.Label(SettinglabelRect(leftIndent, altLines), StringUtils.Localize("#LOC_BDArmory_MaxAltitude") + ": " + ActivePilot.maxAltitude.ToString("0"), Label);//"max altitude" + altLines++; + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(leftIndent, altLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_MaxAlt"), contextLabel);//"max alt" + altLines++; + } + } + GUI.EndGroup(); + altitudeHeight = Mathf.Lerp(altitudeHeight, altLines, 0.15f); + altLines += 0.1f; } - GUI.Label(SettinglabelRect(leftIndent, spdLines), Localizer.Format("#LOC_BDArmory_ABPriority") + ": " + ActivePilot.ABPriority.ToString("0"), Label);//"AB priority" - if (contextTipsEnabled) GUI.Label(ContextLabelRect(leftIndent, ++spdLines), Localizer.Format("#LOC_BDArmory_AIWindow_ABPriority"), contextLabel);//"AB priority context help" - - GUI.EndGroup(); - speedHeight = Mathf.Lerp(speedHeight, ++spdLines, 0.15f); - spdLines += 0.1f; - } - if (showControl) - { - ctrlLines += 0.2f; - GUI.BeginGroup( - new Rect(0, ((pidLines + altLines + spdLines + ctrlLines) * entryHeight), contentWidth, controlHeight * entryHeight), - GUIContent.none, BDArmorySetup.BDGuiSkin.box); - ctrlLines += 0.25f; - - GUI.Label(SettinglabelRect(leftIndent, ctrlLines), Localizer.Format("#LOC_BDArmory_AIWindow_ControlLimits"), BoldLabel);//"Control" - ctrlLines++; - if (!NumFieldsEnabled) - { - ActivePilot.maxSteer = - GUI.HorizontalSlider(SettingSliderRect(leftIndent, ctrlLines, contentWidth), - ActivePilot.maxSteer, 0.1f, 1); - ActivePilot.maxSteer = Mathf.Round(ActivePilot.maxSteer * 20f) / 20f; - } - else + if (showSpeed) { - inputFields["maxSteer"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, ctrlLines, contentWidth), inputFields["maxSteer"].possibleValue, 6)); - ActivePilot.maxSteer = (float)inputFields["maxSteer"].currentValue; - } - GUI.Label(SettinglabelRect(leftIndent, ctrlLines), Localizer.Format("#LOC_BDArmory_AIWindow_LowSpeedSteerLimiter") + ": " + ActivePilot.maxSteer.ToString("0.00"), Label);//"Low speed Limiter" + spdLines += 0.2f; + GUI.BeginGroup( + new Rect(0, ((pidLines + altLines + spdLines) * entryHeight), contentWidth, speedHeight * entryHeight), + GUIContent.none, BDArmorySetup.BDGuiSkin.box); + spdLines += 0.25f; - ctrlLines++; - if (contextTipsEnabled) - { - GUI.Label(ContextLabelRect(leftIndent, ctrlLines), Localizer.Format("#LOC_BDArmory_AIWindow_LSSL"), contextLabel);//"Low limiter context" - ctrlLines++; - } - if (!NumFieldsEnabled) - { - ActivePilot.lowSpeedSwitch = - GUI.HorizontalSlider(SettingSliderRect(leftIndent, ctrlLines, contentWidth), - ActivePilot.lowSpeedSwitch, 10f, 500); - ActivePilot.lowSpeedSwitch = Mathf.Round(ActivePilot.lowSpeedSwitch); - } - else - { - inputFields["lowSpeedSwitch"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, ctrlLines, contentWidth), inputFields["lowSpeedSwitch"].possibleValue, 6)); - ActivePilot.lowSpeedSwitch = (float)inputFields["lowSpeedSwitch"].currentValue; - } - GUI.Label(SettinglabelRect(leftIndent, ctrlLines), Localizer.Format("#LOC_BDArmory_AIWindow_LowSpeedLimiterSpeed") + ": " + ActivePilot.lowSpeedSwitch.ToString("0"), Label);//"dynamic damping max" - - ctrlLines++; - if (contextTipsEnabled) - { - GUI.Label(ContextLabelRect(leftIndent, ctrlLines), Localizer.Format("#LOC_BDArmory_AIWindow_LSLS"), contextLabel);//"dynamic damp max" - ctrlLines++; - } - if (!NumFieldsEnabled) - { - ActivePilot.maxSteerAtMaxSpeed = - GUI.HorizontalSlider(SettingSliderRect(leftIndent, ctrlLines, contentWidth), - ActivePilot.maxSteerAtMaxSpeed, 0.1f, 1); - ActivePilot.maxSteerAtMaxSpeed = Mathf.Round(ActivePilot.maxSteerAtMaxSpeed * 20f) / 20f; - } - else - { - inputFields["maxSteerAtMaxSpeed"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, ctrlLines, contentWidth), inputFields["maxSteerAtMaxSpeed"].possibleValue, 6)); - ActivePilot.maxSteerAtMaxSpeed = (float)inputFields["maxSteerAtMaxSpeed"].currentValue; - } - GUI.Label(SettinglabelRect(leftIndent, ctrlLines), Localizer.Format("#LOC_BDArmory_AIWindow_HighSpeedSteerLimiter") + ": " + ActivePilot.maxSteerAtMaxSpeed.ToString("0.00"), Label);//"dynamic damping min" + GUI.Label(SettinglabelRect(leftIndent, spdLines), StringUtils.Localize("#LOC_BDArmory_PilotAI_Speeds"), BoldLabel);//"Speed" + if (!NumFieldsEnabled) + { + ActivePilot.maxSpeed = GUI.HorizontalSlider(SettingSliderRect(leftIndent, ++spdLines, contentWidth), ActivePilot.maxSpeed, 20, ActivePilot.UpToEleven ? 3000 : 800); + ActivePilot.maxSpeed = Mathf.Round(ActivePilot.maxSpeed / 5) * 5; + } + else + { + inputFields["maxSpeed"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, ++spdLines, contentWidth), inputFields["maxSpeed"].possibleValue, 6, inputFieldStyle)); + ActivePilot.maxSpeed = (float)inputFields["maxSpeed"].currentValue; + } + GUI.Label(SettinglabelRect(leftIndent, spdLines), StringUtils.Localize("#LOC_BDArmory_MaxSpeed") + ": " + ActivePilot.maxSpeed.ToString("0"), Label);//"max speed" + if (contextTipsEnabled) GUI.Label(ContextLabelRect(leftIndent, ++spdLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_maxSpeed"), contextLabel);//"max speed" - ctrlLines++; - if (contextTipsEnabled) - { - GUI.Label(ContextLabelRect(leftIndent, ctrlLines), Localizer.Format("#LOC_BDArmory_AIWindow_HSSL"), contextLabel);//"dynamic damp min" - ctrlLines++; - } - if (!NumFieldsEnabled) - { - ActivePilot.cornerSpeed = - GUI.HorizontalSlider(SettingSliderRect(leftIndent, ctrlLines, contentWidth), - ActivePilot.cornerSpeed, 10, 500); - ActivePilot.cornerSpeed = Mathf.Round(ActivePilot.cornerSpeed); - } - else - { - inputFields["cornerSpeed"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, ctrlLines, contentWidth), inputFields["cornerSpeed"].possibleValue, 6)); - ActivePilot.cornerSpeed = (float)inputFields["cornerSpeed"].currentValue; - } - GUI.Label(SettinglabelRect(leftIndent, ctrlLines), Localizer.Format("#LOC_BDArmory_AIWindow_HighSpeedLimiterSpeed") + ": " + ActivePilot.cornerSpeed.ToString("0"), Label);//"dynamic damping min" + if (!NumFieldsEnabled) + { + ActivePilot.takeOffSpeed = GUI.HorizontalSlider(SettingSliderRect(leftIndent, ++spdLines, contentWidth), ActivePilot.takeOffSpeed, 10f, ActivePilot.UpToEleven ? 2000 : 200); + ActivePilot.takeOffSpeed = Mathf.Round(ActivePilot.takeOffSpeed); + } + else + { + inputFields["takeOffSpeed"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, ++spdLines, contentWidth), inputFields["takeOffSpeed"].possibleValue, 6, inputFieldStyle)); + ActivePilot.takeOffSpeed = (float)inputFields["takeOffSpeed"].currentValue; + } + GUI.Label(SettinglabelRect(leftIndent, spdLines), StringUtils.Localize("#LOC_BDArmory_TakeOffSpeed") + ": " + ActivePilot.takeOffSpeed.ToString("0"), Label);//"takeoff speed" + if (contextTipsEnabled) GUI.Label(ContextLabelRect(leftIndent, ++spdLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_takeoff"), contextLabel);//"takeoff speed help" - ctrlLines++; - if (contextTipsEnabled) - { - GUI.Label(ContextLabelRect(leftIndent, ctrlLines), Localizer.Format("#LOC_BDArmory_AIWindow_HSLS"), contextLabel);//"dynamic damp min" - ctrlLines++; - } - if (!NumFieldsEnabled) - { - ActivePilot.maxBank = - GUI.HorizontalSlider(SettingSliderRect(leftIndent, ctrlLines, contentWidth), - ActivePilot.maxBank, 10, 180); - ActivePilot.maxBank = Mathf.Round(ActivePilot.maxBank / 5) * 5; - } - else - { - inputFields["maxBank"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, ctrlLines, contentWidth), inputFields["maxBank"].possibleValue, 6)); - ActivePilot.maxBank = (float)inputFields["maxBank"].currentValue; - } - GUI.Label(SettinglabelRect(leftIndent, ctrlLines), Localizer.Format("#LOC_BDArmory_BankLimiter") + ": " + ActivePilot.maxBank.ToString("0"), Label);//"dynamic damping min" + if (!NumFieldsEnabled) + { + ActivePilot.minSpeed = GUI.HorizontalSlider(SettingSliderRect(leftIndent, ++spdLines, contentWidth), ActivePilot.minSpeed, 10, ActivePilot.UpToEleven ? 2000 : 200); + ActivePilot.minSpeed = Mathf.Round(ActivePilot.minSpeed); + } + else + { + inputFields["minSpeed"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, ++spdLines, contentWidth), inputFields["minSpeed"].possibleValue, 6, inputFieldStyle)); + ActivePilot.minSpeed = (float)inputFields["minSpeed"].currentValue; + } + GUI.Label(SettinglabelRect(leftIndent, spdLines), StringUtils.Localize("#LOC_BDArmory_MinSpeed") + ": " + ActivePilot.minSpeed.ToString("0"), Label);//"min speed" + if (contextTipsEnabled) GUI.Label(ContextLabelRect(leftIndent, ++spdLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_minSpeed"), contextLabel);//"min speed help" - ctrlLines++; - if (contextTipsEnabled) - { - GUI.Label(ContextLabelRect(leftIndent, ctrlLines), Localizer.Format("#LOC_BDArmory_AIWindow_WPPreRoll"), contextLabel);// Waypoint Pre-Roll Time - ctrlLines++; - } - if (!NumFieldsEnabled) - { - ActivePilot.waypointPreRollTime = - GUI.HorizontalSlider(SettingSliderRect(leftIndent, ctrlLines, contentWidth), - ActivePilot.waypointPreRollTime, 0, 2); - ActivePilot.waypointPreRollTime = Utils.RoundToUnit(ActivePilot.waypointPreRollTime, 0.05f); - } - else - { - inputFields["waypointPreRollTime"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, ctrlLines, contentWidth), inputFields["waypointPreRollTime"].possibleValue, 6)); - ActivePilot.waypointPreRollTime = (float)inputFields["waypointPreRollTime"].currentValue; - } - GUI.Label(SettinglabelRect(leftIndent, ctrlLines), Localizer.Format("#LOC_BDArmory_AIWindow_WaypointPreRollTime") + ": " + ActivePilot.waypointPreRollTime.ToString("0.00"), Label);// + if (!NumFieldsEnabled) + { + ActivePilot.strafingSpeed = GUI.HorizontalSlider(SettingSliderRect(leftIndent, ++spdLines, contentWidth), ActivePilot.strafingSpeed, 10, 200); + ActivePilot.strafingSpeed = Mathf.Round(ActivePilot.strafingSpeed); + } + else + { + inputFields["strafingSpeed"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, ++spdLines, contentWidth), inputFields["strafingSpeed"].possibleValue, 6, inputFieldStyle)); + ActivePilot.strafingSpeed = (float)inputFields["strafingSpeed"].currentValue; + } + GUI.Label(SettinglabelRect(leftIndent, spdLines), StringUtils.Localize("#LOC_BDArmory_StrafingSpeed") + ": " + ActivePilot.strafingSpeed.ToString("0"), Label);//"strafing speed" + if (contextTipsEnabled) GUI.Label(ContextLabelRect(leftIndent, ++spdLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_atkSpeed"), contextLabel);//"strafe speed" - ctrlLines++; - if (contextTipsEnabled) - { - GUI.Label(ContextLabelRect(leftIndent, ctrlLines), Localizer.Format("#LOC_BDArmory_AIWindow_WPYawAuth"), contextLabel);// Waypoint Yaw Authority Time - ctrlLines++; - } - if (!NumFieldsEnabled) - { - ActivePilot.waypointYawAuthorityTime = - GUI.HorizontalSlider(SettingSliderRect(leftIndent, ctrlLines, contentWidth), - ActivePilot.waypointYawAuthorityTime, 0, 10); - ActivePilot.waypointYawAuthorityTime = Utils.RoundToUnit(ActivePilot.waypointYawAuthorityTime, 0.1f); - } - else - { - inputFields["waypointYawAuthorityTime"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, ctrlLines, contentWidth), inputFields["waypointYawAuthorityTime"].possibleValue, 6)); - ActivePilot.waypointYawAuthorityTime = (float)inputFields["waypointYawAuthorityTime"].currentValue; - } - GUI.Label(SettinglabelRect(leftIndent, ctrlLines), Localizer.Format("#LOC_BDArmory_AIWindow_WaypointYawAuthorityTime") + ": " + ActivePilot.waypointYawAuthorityTime.ToString("0.00"), Label);// + if (!NumFieldsEnabled) + { + ActivePilot.idleSpeed = GUI.HorizontalSlider(SettingSliderRect(leftIndent, ++spdLines, contentWidth), ActivePilot.idleSpeed, 10, ActivePilot.UpToEleven ? 3000 : 200); + ActivePilot.idleSpeed = Mathf.Round(ActivePilot.idleSpeed); + } + else + { + inputFields["idleSpeed"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, ++spdLines, contentWidth), inputFields["idleSpeed"].possibleValue, 6, inputFieldStyle)); + ActivePilot.idleSpeed = (float)inputFields["idleSpeed"].currentValue; + } + GUI.Label(SettinglabelRect(leftIndent, spdLines), StringUtils.Localize("#LOC_BDArmory_IdleSpeed") + ": " + ActivePilot.idleSpeed.ToString("0"), Label);//"idle speed" + if (contextTipsEnabled) GUI.Label(ContextLabelRect(leftIndent, ++spdLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_idleSpeed"), contextLabel);//"idle speed context help" - ctrlLines++; - if (contextTipsEnabled) - { - GUI.Label(ContextLabelRect(leftIndent, ctrlLines), Localizer.Format("#LOC_BDArmory_AIWindow_bankLimit"), contextLabel);//"dynamic damp min" - ctrlLines++; - } - if (!NumFieldsEnabled) - { - ActivePilot.maxAllowedGForce = - GUI.HorizontalSlider(SettingSliderRect(leftIndent, ctrlLines, contentWidth), - ActivePilot.maxAllowedGForce, 2, ActivePilot.UpToEleven ? 1000 : 45); - ActivePilot.maxAllowedGForce = Mathf.Round(ActivePilot.maxAllowedGForce * 4f) / 4f; - } - else - { - inputFields["maxAllowedGForce"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, ctrlLines, contentWidth), inputFields["maxAllowedGForce"].possibleValue, 6)); - ActivePilot.maxAllowedGForce = (float)inputFields["maxAllowedGForce"].currentValue; - } - GUI.Label(SettinglabelRect(leftIndent, ctrlLines), Localizer.Format("#LOC_BDArmory_maxAllowedGForce") + ": " + ActivePilot.maxAllowedGForce.ToString("0.00"), Label);//"dynamic damping min" + if (!NumFieldsEnabled) + { + ActivePilot.ABPriority = GUI.HorizontalSlider(SettingSliderRect(leftIndent, ++spdLines, contentWidth), ActivePilot.ABPriority, 0, 100); + ActivePilot.ABPriority = Mathf.Round(ActivePilot.ABPriority); + } + else + { + inputFields["ABPriority"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, ++spdLines, contentWidth), inputFields["ABPriority"].possibleValue, 6, inputFieldStyle)); + ActivePilot.ABPriority = (float)inputFields["ABPriority"].currentValue; + } + GUI.Label(SettinglabelRect(leftIndent, spdLines), StringUtils.Localize("#LOC_BDArmory_ABPriority") + ": " + ActivePilot.ABPriority.ToString("0"), Label);//"AB priority" + if (contextTipsEnabled) GUI.Label(ContextLabelRect(leftIndent, ++spdLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_ABPriority"), contextLabel);//"AB priority context help" - ctrlLines++; - if (contextTipsEnabled) - { - GUI.Label(ContextLabelRect(leftIndent, ctrlLines), Localizer.Format("#LOC_BDArmory_AIWindow_GForce"), contextLabel);//"dynamic damp min" - ctrlLines++; - } - if (!NumFieldsEnabled) - { - ActivePilot.maxAllowedAoA = - GUI.HorizontalSlider(SettingSliderRect(leftIndent, ctrlLines, contentWidth), - ActivePilot.maxAllowedAoA, 0, ActivePilot.UpToEleven ? 180 : 85); - ActivePilot.maxAllowedAoA = Mathf.Round(ActivePilot.maxAllowedAoA * 0.4f) / 0.4f; - } - else - { - inputFields["maxAllowedAoA"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, ctrlLines, contentWidth), inputFields["maxAllowedAoA"].possibleValue, 6)); - ActivePilot.maxAllowedAoA = (float)inputFields["maxAllowedAoA"].currentValue; - } - GUI.Label(SettinglabelRect(leftIndent, ctrlLines), Localizer.Format("#LOC_BDArmory_maxAllowedAoA") + ": " + ActivePilot.maxAllowedAoA.ToString("0.0"), Label);//"dynamic damping min" + if (!NumFieldsEnabled) + { + ActivePilot.ABOverrideThreshold = GUI.HorizontalSlider(SettingSliderRect(leftIndent, ++spdLines, contentWidth), ActivePilot.ABOverrideThreshold, 0, 200); + ActivePilot.ABOverrideThreshold = Mathf.Round(ActivePilot.ABOverrideThreshold); + } + else + { + inputFields["ABOverrideThreshold"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, ++spdLines, contentWidth), inputFields["ABOverrideThreshold"].possibleValue, 6, inputFieldStyle)); + ActivePilot.ABOverrideThreshold = (float)inputFields["ABOverrideThreshold"].currentValue; + } + GUI.Label(SettinglabelRect(leftIndent, spdLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_ABOverrideThreshold") + ": " + ActivePilot.ABOverrideThreshold.ToString("0"), Label);//"AB Override Threshold" + if (contextTipsEnabled) GUI.Label(ContextLabelRect(leftIndent, ++spdLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_ABOverrideThreshold_Context"), contextLabel);//"AB priority context help" - ctrlLines++; - if (contextTipsEnabled) - { - GUI.Label(ContextLabelRect(leftIndent, ctrlLines), Localizer.Format("#LOC_BDArmory_AIWindow_AoA"), contextLabel);//"dynamic damp min" - ctrlLines++; + GUI.EndGroup(); + speedHeight = Mathf.Lerp(speedHeight, ++spdLines, 0.15f); + spdLines += 0.1f; } - GUI.EndGroup(); - controlHeight = Mathf.Lerp(controlHeight, ctrlLines, 0.15f); - ctrlLines += 0.1f; - } - if (showEvade) - { - evadeLines += 0.2f; - GUI.BeginGroup( - new Rect(0, ((pidLines + altLines + spdLines + ctrlLines + evadeLines) * entryHeight), contentWidth, evasionHeight * entryHeight), - GUIContent.none, BDArmorySetup.BDGuiSkin.box); - evadeLines += 0.25f; - - GUI.Label(SettinglabelRect(leftIndent, evadeLines), Localizer.Format("#LOC_BDArmory_AIWindow_EvadeExtend"), BoldLabel);//"Speed" - evadeLines++; - if (!NumFieldsEnabled) + if (showControl) { - ActivePilot.minEvasionTime = - GUI.HorizontalSlider(SettingSliderRect(leftIndent, evadeLines, contentWidth), - ActivePilot.minEvasionTime, 0f, ActivePilot.UpToEleven ? 10 : 1); - ActivePilot.minEvasionTime = Mathf.Round(ActivePilot.minEvasionTime * 20f) / 20f; - } - else - { - inputFields["minEvasionTime"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, evadeLines, contentWidth), inputFields["minEvasionTime"].possibleValue, 6)); - ActivePilot.minEvasionTime = (float)inputFields["minEvasionTime"].currentValue; - } - GUI.Label(SettinglabelRect(leftIndent, evadeLines), Localizer.Format("#LOC_BDArmory_MinEvasionTime") + ": " + ActivePilot.minEvasionTime.ToString("0.00"), Label);//"dynamic damping min" + ctrlLines += 0.2f; + GUI.BeginGroup( + new Rect(0, ((pidLines + altLines + spdLines + ctrlLines) * entryHeight), contentWidth, controlHeight * entryHeight), + GUIContent.none, BDArmorySetup.BDGuiSkin.box); + ctrlLines += 0.25f; + GUI.Label(SettinglabelRect(leftIndent, ctrlLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_ControlLimits"), BoldLabel);//"Control" - evadeLines++; - if (contextTipsEnabled) - { - GUI.Label(ContextLabelRect(leftIndent, evadeLines), Localizer.Format("#LOC_BDArmory_AIWindow_EvExNonlin"), contextLabel); - evadeLines++; - } - if (!NumFieldsEnabled) - { - ActivePilot.evasionNonlinearity = - GUI.HorizontalSlider( - SettingSliderRect(leftIndent, evadeLines, contentWidth), - ActivePilot.evasionNonlinearity, 0, ActivePilot.UpToEleven ? 90 : 10); - ActivePilot.evasionNonlinearity = Mathf.Round(ActivePilot.evasionNonlinearity * 10f) / 10f; - } - else - { - inputFields["evasionNonlinearity"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, evadeLines, contentWidth), inputFields["evasionNonlinearity"].possibleValue, 4)); - ActivePilot.evasionNonlinearity = (float)inputFields["evasionNonlinearity"].currentValue; - } - GUI.Label(SettinglabelRect(leftIndent, evadeLines), Localizer.Format("#LOC_BDArmory_AIWindow_EvasionNonlinearity") + ": " + ActivePilot.evasionNonlinearity.ToString("0.0"), Label);//"Evasion/Extension Nonlinearity" + GUI.Label(SettinglabelRect(leftIndent, ++ctrlLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_LowSpeedSteerLimiter") + ": " + ActivePilot.maxSteer.ToString("0.00"), Label);//"Low speed Limiter" + if (!NumFieldsEnabled) + { + ActivePilot.maxSteer = BDAMath.RoundToUnit(GUI.HorizontalSlider(SettingSliderRect(leftIndent, ctrlLines, contentWidth), ActivePilot.maxSteer, 0.1f, 1), 0.05f); + } + else + { + inputFields["maxSteer"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, ctrlLines, contentWidth), inputFields["maxSteer"].possibleValue, 6, inputFieldStyle)); + ActivePilot.maxSteer = (float)inputFields["maxSteer"].currentValue; + } + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(leftIndent, ++ctrlLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_LSSL"), contextLabel);//"Low limiter context" + } - evadeLines++; - if (contextTipsEnabled) - { - GUI.Label(ContextLabelRect(leftIndent, evadeLines), Localizer.Format("#LOC_BDArmory_AIWindow_MinEvade"), contextLabel);//"dynamic damp min" - evadeLines++; - } - if (!NumFieldsEnabled) - { - ActivePilot.evasionThreshold = - GUI.HorizontalSlider( - SettingSliderRect(leftIndent, evadeLines, contentWidth), - ActivePilot.evasionThreshold, 0, ActivePilot.UpToEleven ? 300 : 100); - ActivePilot.evasionThreshold = Mathf.Round(ActivePilot.evasionThreshold); - } - else - { - inputFields["evasionThreshold"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, evadeLines, contentWidth), inputFields["evasionThreshold"].possibleValue, 6)); - ActivePilot.evasionThreshold = (float)inputFields["evasionThreshold"].currentValue; - } - GUI.Label(SettinglabelRect(leftIndent, evadeLines), Localizer.Format("#LOC_BDArmory_AIWindow_EvasionThreshold") + ": " + ActivePilot.evasionThreshold.ToString("0"), Label);//"dynamic damping max" + GUI.Label(SettinglabelRect(leftIndent, ++ctrlLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_LowSpeedLimiterSpeed") + ": " + ActivePilot.lowSpeedSwitch.ToString("0"), Label);//"dynamic damping max" + if (!NumFieldsEnabled) + { + ActivePilot.lowSpeedSwitch = Mathf.Round(GUI.HorizontalSlider(SettingSliderRect(leftIndent, ctrlLines, contentWidth), ActivePilot.lowSpeedSwitch, 10f, 500)); + } + else + { + inputFields["lowSpeedSwitch"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, ctrlLines, contentWidth), inputFields["lowSpeedSwitch"].possibleValue, 6, inputFieldStyle)); + ActivePilot.lowSpeedSwitch = (float)inputFields["lowSpeedSwitch"].currentValue; + } + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(leftIndent, ++ctrlLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_LSLS"), contextLabel);//"dynamic damp max" + } - evadeLines++; - if (contextTipsEnabled) - { - GUI.Label(ContextLabelRect(leftIndent, evadeLines), Localizer.Format("#LOC_BDArmory_AIWindow_evadeDist"), contextLabel);//"dynamic damp max" - evadeLines++; - } - if (!NumFieldsEnabled) - { - ActivePilot.evasionTimeThreshold = - GUI.HorizontalSlider(SettingSliderRect(leftIndent, evadeLines, contentWidth), - ActivePilot.evasionTimeThreshold, 0, ActivePilot.UpToEleven ? 1 : 3); - ActivePilot.evasionTimeThreshold = Mathf.Round(ActivePilot.evasionTimeThreshold * 100f) / 100f; - } - else - { - inputFields["evasionTimeThreshold"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, evadeLines, contentWidth), inputFields["evasionTimeThreshold"].possibleValue, 6)); - ActivePilot.evasionTimeThreshold = (float)inputFields["evasionTimeThreshold"].currentValue; - } - GUI.Label(SettinglabelRect(leftIndent, evadeLines), Localizer.Format("#LOC_BDArmory_AIWindow_EvasionTimeThreshold") + ": " + ActivePilot.evasionTimeThreshold.ToString("0.00"), Label);//"dynamic damping min" + GUI.Label(SettinglabelRect(leftIndent, ++ctrlLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_HighSpeedSteerLimiter") + ": " + ActivePilot.maxSteerAtMaxSpeed.ToString("0.00"), Label);//"dynamic damping min" + if (!NumFieldsEnabled) + { + ActivePilot.maxSteerAtMaxSpeed = + GUI.HorizontalSlider(SettingSliderRect(leftIndent, ctrlLines, contentWidth), + ActivePilot.maxSteerAtMaxSpeed, 0.1f, 1); + ActivePilot.maxSteerAtMaxSpeed = Mathf.Round(ActivePilot.maxSteerAtMaxSpeed * 20f) / 20f; + } + else + { + inputFields["maxSteerAtMaxSpeed"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, ctrlLines, contentWidth), inputFields["maxSteerAtMaxSpeed"].possibleValue, 6, inputFieldStyle)); + ActivePilot.maxSteerAtMaxSpeed = (float)inputFields["maxSteerAtMaxSpeed"].currentValue; + } + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(leftIndent, ++ctrlLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_HSSL"), contextLabel);//"dynamic damp min" + } - evadeLines++; - ActivePilot.evasionIgnoreMyTargetTargetingMe = GUI.Toggle(ToggleButtonRect(leftIndent, evadeLines, contentWidth), ActivePilot.evasionIgnoreMyTargetTargetingMe, Localizer.Format("#LOC_BDArmory_EvasionIgnoreMyTargetTargetingMe"), ActivePilot.evasionIgnoreMyTargetTargetingMe ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button); + GUI.Label(SettinglabelRect(leftIndent, ++ctrlLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_HighSpeedLimiterSpeed") + ": " + ActivePilot.cornerSpeed.ToString("0"), Label);//"dynamic damping min" + if (!NumFieldsEnabled) + { + ActivePilot.cornerSpeed = Mathf.Round(GUI.HorizontalSlider(SettingSliderRect(leftIndent, ctrlLines, contentWidth), ActivePilot.cornerSpeed, 10, 500)); + } + else + { + inputFields["cornerSpeed"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, ctrlLines, contentWidth), inputFields["cornerSpeed"].possibleValue, 6, inputFieldStyle)); + ActivePilot.cornerSpeed = (float)inputFields["cornerSpeed"].currentValue; + } + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(leftIndent, ++ctrlLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_HSLS"), contextLabel);//"dynamic damp min" + } - evadeLines++; - if (contextTipsEnabled) - { - GUI.Label(ContextLabelRect(leftIndent, evadeLines), Localizer.Format("#LOC_BDArmory_AIWindow_evadetimeDist"), contextLabel);//"dynamic damp min" - evadeLines++; - } - if (!NumFieldsEnabled) - { - ActivePilot.collisionAvoidanceThreshold = - GUI.HorizontalSlider(SettingSliderRect(leftIndent, evadeLines, contentWidth), - ActivePilot.collisionAvoidanceThreshold, 0, 50); - ActivePilot.collisionAvoidanceThreshold = Mathf.Round(ActivePilot.collisionAvoidanceThreshold); - } - else - { - inputFields["collisionAvoidanceThreshold"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, evadeLines, contentWidth), inputFields["collisionAvoidanceThreshold"].possibleValue, 6)); - ActivePilot.collisionAvoidanceThreshold = (float)inputFields["collisionAvoidanceThreshold"].currentValue; - } - GUI.Label(SettinglabelRect(leftIndent, evadeLines), Localizer.Format("#LOC_BDArmory_AIWindow_CollisionAvoidanceThreshold") + ": " + ActivePilot.collisionAvoidanceThreshold.ToString("0"), Label);//"dynamic damping min" + GUI.Label(SettinglabelRect(leftIndent, ++ctrlLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_AltitudeSteerLimiterFactor") + ": " + ActivePilot.altitudeSteerLimiterFactor.ToString("0.00"), Label); + if (!NumFieldsEnabled) + { + ActivePilot.altitudeSteerLimiterFactor = BDAMath.RoundToUnit(GUI.HorizontalSlider(SettingSliderRect(leftIndent, ctrlLines, contentWidth), ActivePilot.altitudeSteerLimiterFactor, -1f, 1f), 0.05f); + } + else + { + inputFields["altitudeSteerLimiterFactor"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, ctrlLines, contentWidth), inputFields["altitudeSteerLimiterFactor"].possibleValue, 6, inputFieldStyle)); + ActivePilot.altitudeSteerLimiterFactor = (float)inputFields["altitudeSteerLimiterFactor"].currentValue; + } + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(leftIndent, ++ctrlLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_ASLF"), contextLabel);//"Altitude Steer Limiter Factor" + } - evadeLines++; - if (contextTipsEnabled) - { - GUI.Label(ContextLabelRect(leftIndent, evadeLines), Localizer.Format("#LOC_BDArmory_AIWindow_ColDist"), contextLabel);//"dynamic damp min" - evadeLines++; - } - if (!NumFieldsEnabled) - { - ActivePilot.vesselCollisionAvoidanceLookAheadPeriod = - GUI.HorizontalSlider(SettingSliderRect(leftIndent, evadeLines, contentWidth), - ActivePilot.vesselCollisionAvoidanceLookAheadPeriod, 0, 3); - ActivePilot.vesselCollisionAvoidanceLookAheadPeriod = Mathf.Round(ActivePilot.vesselCollisionAvoidanceLookAheadPeriod * 10f) / 10f; - } - else - { - inputFields["vesselCollisionAvoidanceLookAheadPeriod"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, evadeLines, contentWidth), inputFields["vesselCollisionAvoidanceLookAheadPeriod"].possibleValue, 6)); - ActivePilot.vesselCollisionAvoidanceLookAheadPeriod = (float)inputFields["vesselCollisionAvoidanceLookAheadPeriod"].currentValue; - } - GUI.Label(SettinglabelRect(leftIndent, evadeLines), Localizer.Format("#LOC_BDArmory_AIWindow_CollisionAvoidanceLookAheadPeriod") + ": " + ActivePilot.vesselCollisionAvoidanceLookAheadPeriod.ToString("0.0"), Label); + GUI.Label(SettinglabelRect(leftIndent, ++ctrlLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_AltitudeSteerLimiterAltitude") + ": " + ActivePilot.altitudeSteerLimiterAltitude.ToString("0"), Label); + if (!NumFieldsEnabled) + { + ActivePilot.altitudeSteerLimiterAltitude = BDAMath.RoundToUnit(GUI.HorizontalSlider(SettingSliderRect(leftIndent, ctrlLines, contentWidth), ActivePilot.altitudeSteerLimiterAltitude, 100f, 10000f), 100f); + } + else + { + inputFields["altitudeSteerLimiterAltitude"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, ctrlLines, contentWidth), inputFields["altitudeSteerLimiterAltitude"].possibleValue, 6, inputFieldStyle)); + ActivePilot.altitudeSteerLimiterAltitude = (float)inputFields["altitudeSteerLimiterAltitude"].currentValue; + } + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(leftIndent, ++ctrlLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_ASLA"), contextLabel);//"Altitude Steer Limiter Altitude" + } - evadeLines++; - if (contextTipsEnabled) - { - GUI.Label(ContextLabelRect(leftIndent, evadeLines), Localizer.Format("#LOC_BDArmory_AIWindow_ColDist"), contextLabel);//"dynamic damp min" - evadeLines++; - } - if (!NumFieldsEnabled) - { - ActivePilot.vesselCollisionAvoidanceStrength = - GUI.HorizontalSlider(SettingSliderRect(leftIndent, evadeLines, contentWidth), - ActivePilot.vesselCollisionAvoidanceStrength, 0, 2); - ActivePilot.vesselCollisionAvoidanceStrength = Mathf.Round(ActivePilot.vesselCollisionAvoidanceStrength * 10f) / 10f; - } - else - { - inputFields["vesselCollisionAvoidanceStrength"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, evadeLines, contentWidth), inputFields["vesselCollisionAvoidanceStrength"].possibleValue, 6)); - ActivePilot.vesselCollisionAvoidanceStrength = (float)inputFields["vesselCollisionAvoidanceStrength"].currentValue; - } - GUI.Label(SettinglabelRect(leftIndent, evadeLines), Localizer.Format("#LOC_BDArmory_AIWindow_CollisionAvoidanceStrength") + ": " + ActivePilot.vesselCollisionAvoidanceStrength.ToString("0.0"), Label); + GUI.Label(SettinglabelRect(leftIndent, ++ctrlLines), StringUtils.Localize("#LOC_BDArmory_BankLimiter") + ": " + ActivePilot.maxBank.ToString("0"), Label);//"dynamic damping min" + if (!NumFieldsEnabled) + { + ActivePilot.maxBank = BDAMath.RoundToUnit(GUI.HorizontalSlider(SettingSliderRect(leftIndent, ctrlLines, contentWidth), ActivePilot.maxBank, 10, (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 55) ? 40 : 180), 5f); + } + else + { + inputFields["maxBank"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, ctrlLines, contentWidth), inputFields["maxBank"].possibleValue, 6, inputFieldStyle)); + ActivePilot.maxBank = (float)inputFields["maxBank"].currentValue; + } + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(leftIndent, ++ctrlLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_bankLimit"), contextLabel);//"dynamic damp min" + } - evadeLines++; - if (contextTipsEnabled) - { - GUI.Label(ContextLabelRect(leftIndent, evadeLines), Localizer.Format("#LOC_BDArmory_AIWindow_ColTime"), contextLabel);//"dynamic damp min" - evadeLines++; - } - if (!NumFieldsEnabled) - { - ActivePilot.vesselStandoffDistance = - GUI.HorizontalSlider(SettingSliderRect(leftIndent, evadeLines, contentWidth), - ActivePilot.vesselStandoffDistance, 2, ActivePilot.UpToEleven ? 5000 : 1000); - ActivePilot.vesselStandoffDistance = Mathf.Round(ActivePilot.vesselStandoffDistance / 50) * 50; - } - else - { - inputFields["vesselStandoffDistance"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, evadeLines, contentWidth), inputFields["vesselStandoffDistance"].possibleValue, 6)); - ActivePilot.vesselStandoffDistance = (float)inputFields["vesselStandoffDistance"].currentValue; - } - GUI.Label(SettinglabelRect(leftIndent, evadeLines), Localizer.Format("#LOC_BDArmory_AIWindow_StandoffDistance") + ": " + ActivePilot.vesselStandoffDistance.ToString("0"), Label);//"dynamic damping min" + GUI.Label(SettinglabelRect(leftIndent, ++ctrlLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_WaypointPreRollTime") + ": " + ActivePilot.waypointPreRollTime.ToString("0.00"), Label);// + if (!NumFieldsEnabled) + { + ActivePilot.waypointPreRollTime = BDAMath.RoundToUnit(GUI.HorizontalSlider(SettingSliderRect(leftIndent, ctrlLines, contentWidth), ActivePilot.waypointPreRollTime, 0, 2), 0.05f); + } + else + { + inputFields["waypointPreRollTime"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, ctrlLines, contentWidth), inputFields["waypointPreRollTime"].possibleValue, 6, inputFieldStyle)); + ActivePilot.waypointPreRollTime = (float)inputFields["waypointPreRollTime"].currentValue; + } + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(leftIndent, ++ctrlLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_WPPreRoll"), contextLabel);// Waypoint Pre-Roll Time + } - evadeLines++; - if (contextTipsEnabled) - { - GUI.Label(ContextLabelRect(leftIndent, evadeLines), Localizer.Format("#LOC_BDArmory_AIWindow_standoff"), contextLabel);//"dynamic damp min" - evadeLines += 1.25f; - } - if (ActivePilot.canExtend) - { - // if (!NumFieldsEnabled) - // { - // ActivePilot.extendMult = - // GUI.HorizontalSlider(SettingSliderRect(leftIndent, evadeLines, contentWidth), - // ActivePilot.extendMult, 0, ActivePilot.UpToEleven ? 200 : 2); - // ActivePilot.extendMult = Mathf.Round(ActivePilot.extendMult * 10f) / 10f; - // } - // else - // { - // inputFields["extendMult"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, evadeLines, contentWidth), inputFields["extendMult"].possibleValue, 6)); - // ActivePilot.extendMult = (float)inputFields["extendMult"].currentValue; - // } - // GUI.Label(SettinglabelRect(leftIndent, evadeLines), Localizer.Format("#LOC_BDArmory_AIWindow_ExtendMultiplier") + ": " + ActivePilot.extendMult.ToString("0.0"), Label);//"dynamic damping min" - // evadeLines++; - // if (contextTipsEnabled) - // { - // GUI.Label(ContextLabelRect(leftIndent, evadeLines), Localizer.Format("#LOC_BDArmory_AIWindow_ExtendMult"), contextLabel);//"dynamic damp min" - // evadeLines++; - // } + GUI.Label(SettinglabelRect(leftIndent, ++ctrlLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_WaypointYawAuthorityTime") + ": " + ActivePilot.waypointYawAuthorityTime.ToString("0.00"), Label);// + if (!NumFieldsEnabled) + { + ActivePilot.waypointYawAuthorityTime = BDAMath.RoundToUnit(GUI.HorizontalSlider(SettingSliderRect(leftIndent, ctrlLines, contentWidth), ActivePilot.waypointYawAuthorityTime, 0, 10), 0.1f); + } + else + { + inputFields["waypointYawAuthorityTime"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, ctrlLines, contentWidth), inputFields["waypointYawAuthorityTime"].possibleValue, 6, inputFieldStyle)); + ActivePilot.waypointYawAuthorityTime = (float)inputFields["waypointYawAuthorityTime"].currentValue; + } + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(leftIndent, ++ctrlLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_WPYawAuth"), contextLabel);// Waypoint Yaw Authority Time + } + GUI.Label(SettinglabelRect(leftIndent, ++ctrlLines), StringUtils.Localize("#LOC_BDArmory_maxAllowedGForce") + ": " + ActivePilot.maxAllowedGForce.ToString("0.00"), Label); if (!NumFieldsEnabled) { - ActivePilot.extendDistanceAirToAir = - GUI.HorizontalSlider(SettingSliderRect(leftIndent, evadeLines, contentWidth), - ActivePilot.extendDistanceAirToAir, 0, ActivePilot.UpToEleven ? 20000 : 2000); - ActivePilot.extendDistanceAirToAir = Utils.RoundToUnit(ActivePilot.extendDistanceAirToAir, 50f); + ActivePilot.maxAllowedGForce = BDAMath.RoundToUnit(GUI.HorizontalSlider(SettingSliderRect(leftIndent, ctrlLines, contentWidth), ActivePilot.maxAllowedGForce, 2, ActivePilot.UpToEleven ? 1000 : 45), 0.25f); } else { - inputFields["extendDistanceAirToAir"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, evadeLines, contentWidth), inputFields["extendDistanceAirToAir"].possibleValue, 6)); - ActivePilot.extendDistanceAirToAir = (float)inputFields["extendDistanceAirToAir"].currentValue; + inputFields["maxAllowedGForce"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, ctrlLines, contentWidth), inputFields["maxAllowedGForce"].possibleValue, 6, inputFieldStyle)); + ActivePilot.maxAllowedGForce = (float)inputFields["maxAllowedGForce"].currentValue; } - GUI.Label(SettinglabelRect(leftIndent, evadeLines), Localizer.Format("#LOC_BDArmory_AIWindow_ExtendDistanceAirToAir") + ": " + ActivePilot.extendDistanceAirToAir.ToString("0"), Label); // Extend Distance Air-To-Air - evadeLines++; if (contextTipsEnabled) { - GUI.Label(ContextLabelRect(leftIndent, evadeLines), Localizer.Format("#LOC_BDArmory_AIWindow_ExtendDistanceAirToAir_Context"), contextLabel); - evadeLines++; + GUI.Label(ContextLabelRect(leftIndent, ++ctrlLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_GForce"), contextLabel); } + GUI.Label(SettinglabelRect(leftIndent, ++ctrlLines), StringUtils.Localize("#LOC_BDArmory_maxAllowedAoA") + ": " + ActivePilot.maxAllowedAoA.ToString("0.0"), Label); if (!NumFieldsEnabled) { - ActivePilot.extendDistanceAirToGroundGuns = - GUI.HorizontalSlider(SettingSliderRect(leftIndent, evadeLines, contentWidth), - ActivePilot.extendDistanceAirToGroundGuns, 0, ActivePilot.UpToEleven ? 20000 : 5000); - ActivePilot.extendDistanceAirToGroundGuns = Utils.RoundToUnit(ActivePilot.extendDistanceAirToGroundGuns, 100f); + ActivePilot.maxAllowedAoA = BDAMath.RoundToUnit(GUI.HorizontalSlider(SettingSliderRect(leftIndent, ctrlLines, contentWidth), ActivePilot.maxAllowedAoA, 0f, ActivePilot.UpToEleven ? 180f : 90f), 2.5f); } else { - inputFields["extendDistanceAirToGroundGuns"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, evadeLines, contentWidth), inputFields["extendDistanceAirToGroundGuns"].possibleValue, 6)); - ActivePilot.extendDistanceAirToGroundGuns = (float)inputFields["extendDistanceAirToGroundGuns"].currentValue; + inputFields["maxAllowedAoA"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, ctrlLines, contentWidth), inputFields["maxAllowedAoA"].possibleValue, 6, inputFieldStyle)); + ActivePilot.maxAllowedAoA = (float)inputFields["maxAllowedAoA"].currentValue; } - GUI.Label(SettinglabelRect(leftIndent, evadeLines), Localizer.Format("#LOC_BDArmory_AIWindow_ExtendDistanceAirToGroundGuns") + ": " + ActivePilot.extendDistanceAirToGroundGuns.ToString("0"), Label); // Extend Distance Air-To-Ground (Guns) - evadeLines++; if (contextTipsEnabled) { - GUI.Label(ContextLabelRect(leftIndent, evadeLines), Localizer.Format("#LOC_BDArmory_AIWindow_ExtendDistanceAirToGroundGuns_Context"), contextLabel); - evadeLines++; + GUI.Label(ContextLabelRect(leftIndent, ++ctrlLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_AoA"), contextLabel); + } + + if (!(BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 55)) + { + GUI.Label(SettinglabelRect(leftIndent, ++ctrlLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_postStallAoA") + ": " + ActivePilot.postStallAoA.ToString("0.0"), Label); + if (!NumFieldsEnabled) + { + ActivePilot.postStallAoA = BDAMath.RoundToUnit(GUI.HorizontalSlider(SettingSliderRect(leftIndent, ctrlLines, contentWidth), ActivePilot.postStallAoA, 0f, ActivePilot.UpToEleven ? 180f : 90f), 2.5f); + } + else + { + inputFields["postStallAoA"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, ctrlLines, contentWidth), inputFields["postStallAoA"].possibleValue, 6, inputFieldStyle)); + ActivePilot.postStallAoA = (float)inputFields["postStallAoA"].currentValue; + } + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(leftIndent, ++ctrlLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_AoAPostStall"), contextLabel); + } } + GUI.Label(SettinglabelRect(leftIndent, ++ctrlLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_ImmelmannTurnAngle") + ": " + ActivePilot.ImmelmannTurnAngle.ToString("0"), Label); if (!NumFieldsEnabled) { - ActivePilot.extendDistanceAirToGround = - GUI.HorizontalSlider(SettingSliderRect(leftIndent, evadeLines, contentWidth), - ActivePilot.extendDistanceAirToGround, 0, ActivePilot.UpToEleven ? 20000 : 5000); - ActivePilot.extendDistanceAirToGround = Utils.RoundToUnit(ActivePilot.extendDistanceAirToGround, 100f); + ActivePilot.ImmelmannTurnAngle = Mathf.Round(GUI.HorizontalSlider(SettingSliderRect(leftIndent, ctrlLines, contentWidth), ActivePilot.ImmelmannTurnAngle, 0f, 90f)); } else { - inputFields["extendDistanceAirToGround"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, evadeLines, contentWidth), inputFields["extendDistanceAirToGround"].possibleValue, 6)); - ActivePilot.extendDistanceAirToGround = (float)inputFields["extendDistanceAirToGround"].currentValue; + inputFields["ImmelmannTurnAngle"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, ctrlLines, contentWidth), inputFields["ImmelmannTurnAngle"].possibleValue, 6, inputFieldStyle)); + ActivePilot.ImmelmannTurnAngle = (float)inputFields["ImmelmannTurnAngle"].currentValue; } - GUI.Label(SettinglabelRect(leftIndent, evadeLines), Localizer.Format("#LOC_BDArmory_AIWindow_ExtendDistanceAirToGround") + ": " + ActivePilot.extendDistanceAirToGround.ToString("0"), Label); // Extend Distance Air-To-Ground - evadeLines++; if (contextTipsEnabled) { - GUI.Label(ContextLabelRect(leftIndent, evadeLines), Localizer.Format("#LOC_BDArmory_AIWindow_ExtendDistanceAirToGround_Context"), contextLabel); - evadeLines++; + GUI.Label(ContextLabelRect(leftIndent, ++ctrlLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_ImmelmannTurnAngleContext"), contextLabel); } + ++ctrlLines; + GUI.EndGroup(); + controlHeight = Mathf.Lerp(controlHeight, ctrlLines, 0.15f); + ctrlLines += 0.1f; + } + + if (showEvade) + { + evadeLines += 0.2f; + GUI.BeginGroup( + new Rect(0, ((pidLines + altLines + spdLines + ctrlLines + evadeLines) * entryHeight), contentWidth, evasionHeight * entryHeight), + GUIContent.none, BDArmorySetup.BDGuiSkin.box); + #region Evasion + evadeLines += 0.25f; + GUI.Label(SettinglabelRect(leftIndent, evadeLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_Evade"), BoldLabel); + + GUI.Label(SettinglabelRect(leftIndent, ++evadeLines), $"{StringUtils.Localize("#LOC_BDArmory_MinEvasionTime")}: {ActivePilot.minEvasionTime:0.00}s", Label); if (!NumFieldsEnabled) { - ActivePilot.extendTargetVel = + ActivePilot.minEvasionTime = GUI.HorizontalSlider(SettingSliderRect(leftIndent, evadeLines, contentWidth), - ActivePilot.extendTargetVel, 0, 2); - ActivePilot.extendTargetVel = Mathf.Round(ActivePilot.extendTargetVel * 10f) / 10f; + ActivePilot.minEvasionTime, 0f, ActivePilot.UpToEleven ? 10 : 1); + ActivePilot.minEvasionTime = Mathf.Round(ActivePilot.minEvasionTime * 20f) / 20f; } else { - inputFields["extendTargetVel"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, evadeLines, contentWidth), inputFields["extendTargetVel"].possibleValue, 6)); - ActivePilot.extendTargetVel = (float)inputFields["extendTargetVel"].currentValue; + inputFields["minEvasionTime"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, evadeLines, contentWidth), inputFields["minEvasionTime"].possibleValue, 6, inputFieldStyle)); + ActivePilot.minEvasionTime = (float)inputFields["minEvasionTime"].currentValue; } - GUI.Label(SettinglabelRect(leftIndent, evadeLines), Localizer.Format("#LOC_BDArmory_AIWindow_ExtendTargetVel") + ": " + ActivePilot.extendTargetVel.ToString("0.0"), Label);//"dynamic damping min" - evadeLines++; - if (contextTipsEnabled) + if (contextTipsEnabled) GUI.Label(ContextLabelRect(leftIndent, ++evadeLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_MinEvade"), contextLabel); + + GUI.Label(SettinglabelRect(leftIndent, ++evadeLines), $"{StringUtils.Localize("#LOC_BDArmory_AIWindow_EvasionThreshold")}: {ActivePilot.evasionThreshold:0}m", Label); + if (!NumFieldsEnabled) + { + ActivePilot.evasionThreshold = + GUI.HorizontalSlider( + SettingSliderRect(leftIndent, evadeLines, contentWidth), + ActivePilot.evasionThreshold, 0, ActivePilot.UpToEleven ? 300 : 100); + ActivePilot.evasionThreshold = Mathf.Round(ActivePilot.evasionThreshold); + } + else { - GUI.Label(ContextLabelRect(leftIndent, evadeLines), Localizer.Format("#LOC_BDArmory_AIWindow_Extendvel"), contextLabel);//"dynamic damp min" - evadeLines++; + inputFields["evasionThreshold"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, evadeLines, contentWidth), inputFields["evasionThreshold"].possibleValue, 6, inputFieldStyle)); + ActivePilot.evasionThreshold = (float)inputFields["evasionThreshold"].currentValue; } + if (contextTipsEnabled) GUI.Label(ContextLabelRect(leftIndent, ++evadeLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_evadeDist"), contextLabel); + GUI.Label(SettinglabelRect(leftIndent, ++evadeLines), $"{StringUtils.Localize("#LOC_BDArmory_AIWindow_EvasionTimeThreshold")}: {ActivePilot.evasionTimeThreshold:0.00}s", Label); if (!NumFieldsEnabled) { - ActivePilot.extendTargetAngle = + ActivePilot.evasionTimeThreshold = GUI.HorizontalSlider(SettingSliderRect(leftIndent, evadeLines, contentWidth), - ActivePilot.extendTargetAngle, 0, 180); - ActivePilot.extendTargetAngle = Mathf.Round(ActivePilot.extendTargetAngle); + ActivePilot.evasionTimeThreshold, 0, ActivePilot.UpToEleven ? 1 : 3); + ActivePilot.evasionTimeThreshold = Mathf.Round(ActivePilot.evasionTimeThreshold * 100f) / 100f; } else { - inputFields["extendTargetAngle"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, evadeLines, contentWidth), inputFields["extendTargetAngle"].possibleValue, 6)); - ActivePilot.extendTargetAngle = (float)inputFields["extendTargetAngle"].currentValue; + inputFields["evasionTimeThreshold"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, evadeLines, contentWidth), inputFields["evasionTimeThreshold"].possibleValue, 6, inputFieldStyle)); + ActivePilot.evasionTimeThreshold = (float)inputFields["evasionTimeThreshold"].currentValue; } - GUI.Label(SettinglabelRect(leftIndent, evadeLines), Localizer.Format("#LOC_BDArmory_AIWindow_ExtendTargetAngle") + ": " + ActivePilot.extendTargetAngle.ToString("0"), Label);// "dynamic damping min" - evadeLines++; - if (contextTipsEnabled) + if (contextTipsEnabled) GUI.Label(ContextLabelRect(leftIndent, ++evadeLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_evadetimeDist"), contextLabel); + + GUI.Label(SettinglabelRect(leftIndent, ++evadeLines), $"{StringUtils.Localize("#LOC_BDArmory_AIWindow_EvasionNonlinearity")}: {ActivePilot.evasionNonlinearity:0.0}°", Label);//"Evasion/Extension Nonlinearity" + if (!NumFieldsEnabled) + { + ActivePilot.evasionNonlinearity = + GUI.HorizontalSlider( + SettingSliderRect(leftIndent, evadeLines, contentWidth), + ActivePilot.evasionNonlinearity, 0, ActivePilot.UpToEleven ? 90 : 10); + ActivePilot.evasionNonlinearity = Mathf.Round(ActivePilot.evasionNonlinearity * 10f) / 10f; + } + else + { + inputFields["evasionNonlinearity"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, evadeLines, contentWidth), inputFields["evasionNonlinearity"].possibleValue, 4, inputFieldStyle)); + ActivePilot.evasionNonlinearity = (float)inputFields["evasionNonlinearity"].currentValue; + } + if (contextTipsEnabled) GUI.Label(ContextLabelRect(leftIndent, ++evadeLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_EvExNonlin"), contextLabel); + + ActivePilot.evasionIgnoreMyTargetTargetingMe = GUI.Toggle(ToggleButtonRect(leftIndent, ++evadeLines, contentWidth), ActivePilot.evasionIgnoreMyTargetTargetingMe, StringUtils.Localize("#LOC_BDArmory_EvasionIgnoreMyTargetTargetingMe"), ActivePilot.evasionIgnoreMyTargetTargetingMe ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button); + #endregion + + #region Craft Avoidance + evadeLines += 1.5f; + GUI.Label(SettinglabelRect(leftIndent, evadeLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_Avoidance"), BoldLabel); + + GUI.Label(SettinglabelRect(leftIndent, ++evadeLines), $"{StringUtils.Localize("#LOC_BDArmory_AIWindow_CollisionAvoidanceThreshold")}: {ActivePilot.collisionAvoidanceThreshold:0}m", Label); + if (!NumFieldsEnabled) + { + ActivePilot.collisionAvoidanceThreshold = + GUI.HorizontalSlider(SettingSliderRect(leftIndent, evadeLines, contentWidth), + ActivePilot.collisionAvoidanceThreshold, 0, 50); + ActivePilot.collisionAvoidanceThreshold = Mathf.Round(ActivePilot.collisionAvoidanceThreshold); + } + else { - GUI.Label(ContextLabelRect(leftIndent, evadeLines), Localizer.Format("#LOC_BDArmory_AIWindow_ExtendAngle"), contextLabel);//"dynamic damp min" - evadeLines++; + inputFields["collisionAvoidanceThreshold"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, evadeLines, contentWidth), inputFields["collisionAvoidanceThreshold"].possibleValue, 6, inputFieldStyle)); + ActivePilot.collisionAvoidanceThreshold = (float)inputFields["collisionAvoidanceThreshold"].currentValue; } + if (contextTipsEnabled) GUI.Label(ContextLabelRect(leftIndent, ++evadeLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_ColDist"), contextLabel); + GUI.Label(SettinglabelRect(leftIndent, ++evadeLines), $"{StringUtils.Localize("#LOC_BDArmory_AIWindow_CollisionAvoidanceLookAheadPeriod")}: {ActivePilot.vesselCollisionAvoidanceLookAheadPeriod:0.0}s", Label); if (!NumFieldsEnabled) { - ActivePilot.extendTargetDist = + ActivePilot.vesselCollisionAvoidanceLookAheadPeriod = GUI.HorizontalSlider(SettingSliderRect(leftIndent, evadeLines, contentWidth), - ActivePilot.extendTargetDist, 0, 5000); - ActivePilot.extendTargetDist = Mathf.Round(ActivePilot.extendTargetDist / 25) * 25; + ActivePilot.vesselCollisionAvoidanceLookAheadPeriod, 0, 3); + ActivePilot.vesselCollisionAvoidanceLookAheadPeriod = Mathf.Round(ActivePilot.vesselCollisionAvoidanceLookAheadPeriod * 10f) / 10f; } else { - inputFields["extendTargetDist"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, evadeLines, contentWidth), inputFields["extendTargetDist"].possibleValue, 6)); - ActivePilot.extendTargetDist = (float)inputFields["extendTargetDist"].currentValue; + inputFields["vesselCollisionAvoidanceLookAheadPeriod"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, evadeLines, contentWidth), inputFields["vesselCollisionAvoidanceLookAheadPeriod"].possibleValue, 6, inputFieldStyle)); + ActivePilot.vesselCollisionAvoidanceLookAheadPeriod = (float)inputFields["vesselCollisionAvoidanceLookAheadPeriod"].currentValue; } - GUI.Label(SettinglabelRect(leftIndent, evadeLines), Localizer.Format("#LOC_BDArmory_AIWindow_ExtendTargetDist") + ": " + ActivePilot.extendTargetDist.ToString("0.00"), Label);//"dynamic damping min" - evadeLines++; - if (contextTipsEnabled) + if (contextTipsEnabled) GUI.Label(ContextLabelRect(leftIndent, ++evadeLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_ColTime"), contextLabel); + + GUI.Label(SettinglabelRect(leftIndent, ++evadeLines), $"{StringUtils.Localize("#LOC_BDArmory_AIWindow_CollisionAvoidanceStrength")}: {ActivePilot.vesselCollisionAvoidanceStrength:0.0} ({ActivePilot.vesselCollisionAvoidanceStrength / Time.fixedDeltaTime:0}°/s)", Label); + if (!NumFieldsEnabled) + { + ActivePilot.vesselCollisionAvoidanceStrength = + GUI.HorizontalSlider(SettingSliderRect(leftIndent, evadeLines, contentWidth), + ActivePilot.vesselCollisionAvoidanceStrength, 0, 4); + ActivePilot.vesselCollisionAvoidanceStrength = BDAMath.RoundToUnit(ActivePilot.vesselCollisionAvoidanceStrength, 0.1f); + } + else { - GUI.Label(ContextLabelRect(leftIndent, evadeLines), Localizer.Format("#LOC_BDArmory_AIWindow_ExtendDist"), contextLabel);//"dynamic damp min" - evadeLines++; + inputFields["vesselCollisionAvoidanceStrength"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, evadeLines, contentWidth), inputFields["vesselCollisionAvoidanceStrength"].possibleValue, 6, inputFieldStyle)); + ActivePilot.vesselCollisionAvoidanceStrength = (float)inputFields["vesselCollisionAvoidanceStrength"].currentValue; } - } - ActivePilot.canExtend = GUI.Toggle(ToggleButtonRect(leftIndent, evadeLines, contentWidth), ActivePilot.canExtend, Localizer.Format("#LOC_BDArmory_ExtendToggle"), ActivePilot.canExtend ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Dynamic pid" - evadeLines++; + if (contextTipsEnabled) GUI.Label(ContextLabelRect(leftIndent, ++evadeLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_ColStrength"), contextLabel); - GUI.EndGroup(); - evasionHeight = Mathf.Lerp(evasionHeight, evadeLines, 0.15f); - evadeLines += 0.1f; - } + GUI.Label(SettinglabelRect(leftIndent, ++evadeLines), $"{StringUtils.Localize("#LOC_BDArmory_AIWindow_StandoffDistance")}: {ActivePilot.vesselStandoffDistance:0}m", Label); + if (!NumFieldsEnabled) + { + ActivePilot.vesselStandoffDistance = + GUI.HorizontalSlider(SettingSliderRect(leftIndent, evadeLines, contentWidth), + ActivePilot.vesselStandoffDistance, 2, ActivePilot.UpToEleven ? 5000 : 1000); + ActivePilot.vesselStandoffDistance = Mathf.Round(ActivePilot.vesselStandoffDistance / 50) * 50; + } + else + { + inputFields["vesselStandoffDistance"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, evadeLines, contentWidth), inputFields["vesselStandoffDistance"].possibleValue, 6, inputFieldStyle)); + ActivePilot.vesselStandoffDistance = (float)inputFields["vesselStandoffDistance"].currentValue; + } + if (contextTipsEnabled) GUI.Label(ContextLabelRect(leftIndent, ++evadeLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_standoff"), contextLabel); + #endregion - if (showTerrain) - { - gndLines += 0.2f; - GUI.BeginGroup( - new Rect(0, ((pidLines + altLines + spdLines + ctrlLines + evadeLines + gndLines) * entryHeight), contentWidth, terrainHeight * entryHeight), - GUIContent.none, BDArmorySetup.BDGuiSkin.box); - gndLines += 0.25f; - - GUI.Label(SettinglabelRect(leftIndent, gndLines), Localizer.Format("#LOC_BDArmory_PilotAI_Terrain"), BoldLabel);//"Speed" - gndLines++; - var oldMinTwiddle = ActivePilot.turnRadiusTwiddleFactorMin; - if (!NumFieldsEnabled) - { - ActivePilot.turnRadiusTwiddleFactorMin = - GUI.HorizontalSlider(SettingSliderRect(leftIndent, gndLines, contentWidth), - ActivePilot.turnRadiusTwiddleFactorMin, 0.1f, ActivePilot.UpToEleven ? 10 : 5); - ActivePilot.turnRadiusTwiddleFactorMin = Mathf.Round(ActivePilot.turnRadiusTwiddleFactorMin * 10f) / 10f; - } - else - { - inputFields["turnRadiusTwiddleFactorMin"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, gndLines, contentWidth), inputFields["turnRadiusTwiddleFactorMin"].possibleValue, 6)); - ActivePilot.turnRadiusTwiddleFactorMin = (float)inputFields["turnRadiusTwiddleFactorMin"].currentValue; - } - if (ActivePilot.turnRadiusTwiddleFactorMin != oldMinTwiddle) - { - ActivePilot.OnMinUpdated(null, null); - inputFields["turnRadiusTwiddleFactorMax"].currentValue = ActivePilot.turnRadiusTwiddleFactorMax; - } - GUI.Label(SettinglabelRect(leftIndent, gndLines), Localizer.Format("#LOC_BDArmory_AIWindow_TurnRadiusMin") + ": " + ActivePilot.turnRadiusTwiddleFactorMin.ToString("0.0"), Label); //"dynamic damping min" + #region Extending + if (ActivePilot.canExtend) + { + evadeLines += 1.5f; + GUI.Label(SettinglabelRect(leftIndent, evadeLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_Extend"), BoldLabel); + #region Extend Distance Air-to-Air + GUI.Label(SettinglabelRect(leftIndent, ++evadeLines), $"{StringUtils.Localize("#LOC_BDArmory_AIWindow_ExtendDistanceAirToAir")}: {ActivePilot.extendDistanceAirToAir:0}m", Label); // Extend Distance Air-To-Air + if (!NumFieldsEnabled) + { + ActivePilot.extendDistanceAirToAir = + GUI.HorizontalSlider(SettingSliderRect(leftIndent, evadeLines, contentWidth), + ActivePilot.extendDistanceAirToAir, 0, ActivePilot.UpToEleven ? 20000 : 2000); + ActivePilot.extendDistanceAirToAir = BDAMath.RoundToUnit(ActivePilot.extendDistanceAirToAir, 10f); + } + else + { + inputFields["extendDistanceAirToAir"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, evadeLines, contentWidth), inputFields["extendDistanceAirToAir"].possibleValue, 6, inputFieldStyle)); + ActivePilot.extendDistanceAirToAir = (float)inputFields["extendDistanceAirToAir"].currentValue; + } + if (contextTipsEnabled) GUI.Label(ContextLabelRect(leftIndent, ++evadeLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_ExtendDistanceAirToAir_Context"), contextLabel); + #endregion - gndLines++; - if (contextTipsEnabled) - { - GUI.Label(ContextLabelRect(leftIndent, gndLines), Localizer.Format("#LOC_BDArmory_AIWindow_terrainMin"), contextLabel);//"dynamic damp min" - gndLines++; - } - var oldMaxTwiddle = ActivePilot.turnRadiusTwiddleFactorMax; - if (!NumFieldsEnabled) - { - ActivePilot.turnRadiusTwiddleFactorMax = - GUI.HorizontalSlider(SettingSliderRect(leftIndent, gndLines, contentWidth), - ActivePilot.turnRadiusTwiddleFactorMax, 0.1f, ActivePilot.UpToEleven ? 10 : 5); - ActivePilot.turnRadiusTwiddleFactorMax = Mathf.Round(ActivePilot.turnRadiusTwiddleFactorMax * 10) / 10; - } - else - { - inputFields["turnRadiusTwiddleFactorMax"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, gndLines, contentWidth), inputFields["turnRadiusTwiddleFactorMax"].possibleValue, 6)); - ActivePilot.turnRadiusTwiddleFactorMax = (float)inputFields["turnRadiusTwiddleFactorMax"].currentValue; - } - if (ActivePilot.turnRadiusTwiddleFactorMax != oldMaxTwiddle) - { - ActivePilot.OnMaxUpdated(null, null); - inputFields["turnRadiusTwiddleFactorMin"].currentValue = ActivePilot.turnRadiusTwiddleFactorMin; + #region Extend Angle Air-to-Air + GUI.Label(SettinglabelRect(leftIndent, ++evadeLines), $"{StringUtils.Localize("#LOC_BDArmory_AIWindow_ExtendAngleAirToAir")}: {ActivePilot.extendAngleAirToAir:0}°", Label); // Extend Angle Air-To-Air + if (!NumFieldsEnabled) + { + ActivePilot.extendAngleAirToAir = + GUI.HorizontalSlider(SettingSliderRect(leftIndent, evadeLines, contentWidth), + ActivePilot.extendAngleAirToAir, ActivePilot.UpToEleven ? -90 : -10, ActivePilot.UpToEleven ? 90 : 45); + ActivePilot.extendAngleAirToAir = BDAMath.RoundToUnit(ActivePilot.extendAngleAirToAir, 1f); + } + else + { + inputFields["extendAngleAirToAir"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, evadeLines, contentWidth), inputFields["extendAngleAirToAir"].possibleValue, 6, inputFieldStyle)); + ActivePilot.extendAngleAirToAir = (float)inputFields["extendAngleAirToAir"].currentValue; + } + if (contextTipsEnabled) GUI.Label(ContextLabelRect(leftIndent, ++evadeLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_ExtendAngleAirToAir_Context"), contextLabel); + #endregion + + #region Extend Distance Air-to-Ground (Guns) + GUI.Label(SettinglabelRect(leftIndent, ++evadeLines), $"{StringUtils.Localize("#LOC_BDArmory_AIWindow_ExtendDistanceAirToGroundGuns")}: {ActivePilot.extendDistanceAirToGroundGuns:0}m", Label); // Extend Distance Air-To-Ground (Guns) + if (!NumFieldsEnabled) + { + ActivePilot.extendDistanceAirToGroundGuns = + GUI.HorizontalSlider(SettingSliderRect(leftIndent, evadeLines, contentWidth), + ActivePilot.extendDistanceAirToGroundGuns, 0, ActivePilot.UpToEleven ? 20000 : 5000); + ActivePilot.extendDistanceAirToGroundGuns = BDAMath.RoundToUnit(ActivePilot.extendDistanceAirToGroundGuns, 50f); + } + else + { + inputFields["extendDistanceAirToGroundGuns"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, evadeLines, contentWidth), inputFields["extendDistanceAirToGroundGuns"].possibleValue, 6, inputFieldStyle)); + ActivePilot.extendDistanceAirToGroundGuns = (float)inputFields["extendDistanceAirToGroundGuns"].currentValue; + } + if (contextTipsEnabled) GUI.Label(ContextLabelRect(leftIndent, ++evadeLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_ExtendDistanceAirToGroundGuns_Context"), contextLabel); + #endregion + + #region Extend Distance Air-to-Ground + GUI.Label(SettinglabelRect(leftIndent, ++evadeLines), $"{StringUtils.Localize("#LOC_BDArmory_AIWindow_ExtendDistanceAirToGround")}: {ActivePilot.extendDistanceAirToGround:0}m", Label); // Extend Distance Air-To-Ground + if (!NumFieldsEnabled) + { + ActivePilot.extendDistanceAirToGround = + GUI.HorizontalSlider(SettingSliderRect(leftIndent, evadeLines, contentWidth), + ActivePilot.extendDistanceAirToGround, 0, ActivePilot.UpToEleven ? 20000 : 5000); + ActivePilot.extendDistanceAirToGround = BDAMath.RoundToUnit(ActivePilot.extendDistanceAirToGround, 50f); + } + else + { + inputFields["extendDistanceAirToGround"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, evadeLines, contentWidth), inputFields["extendDistanceAirToGround"].possibleValue, 6, inputFieldStyle)); + ActivePilot.extendDistanceAirToGround = (float)inputFields["extendDistanceAirToGround"].currentValue; + } + if (contextTipsEnabled) GUI.Label(ContextLabelRect(leftIndent, ++evadeLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_ExtendDistanceAirToGround_Context"), contextLabel); + #endregion + + #region Extend Target triggers + GUI.Label(SettinglabelRect(leftIndent, ++evadeLines), $"{StringUtils.Localize("#LOC_BDArmory_AIWindow_ExtendTargetVel")}: {ActivePilot.extendTargetVel:0.0}", Label); + if (!NumFieldsEnabled) + { + ActivePilot.extendTargetVel = GUI.HorizontalSlider(SettingSliderRect(leftIndent, evadeLines, contentWidth), ActivePilot.extendTargetVel, 0, 2); + ActivePilot.extendTargetVel = BDAMath.RoundToUnit(ActivePilot.extendTargetVel, 0.1f); + } + else + { + inputFields["extendTargetVel"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, evadeLines, contentWidth), inputFields["extendTargetVel"].possibleValue, 6, inputFieldStyle)); + ActivePilot.extendTargetVel = (float)inputFields["extendTargetVel"].currentValue; + } + if (contextTipsEnabled) GUI.Label(ContextLabelRect(leftIndent, ++evadeLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_Extendvel"), contextLabel); + + GUI.Label(SettinglabelRect(leftIndent, ++evadeLines), $"{StringUtils.Localize("#LOC_BDArmory_AIWindow_ExtendTargetAngle")}: {ActivePilot.extendTargetAngle}°", Label); + if (!NumFieldsEnabled) + { + ActivePilot.extendTargetAngle = GUI.HorizontalSlider(SettingSliderRect(leftIndent, evadeLines, contentWidth), ActivePilot.extendTargetAngle, 0, 180); + ActivePilot.extendTargetAngle = Mathf.Round(ActivePilot.extendTargetAngle); + } + else + { + inputFields["extendTargetAngle"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, evadeLines, contentWidth), inputFields["extendTargetAngle"].possibleValue, 6, inputFieldStyle)); + ActivePilot.extendTargetAngle = (float)inputFields["extendTargetAngle"].currentValue; + } + if (contextTipsEnabled) GUI.Label(ContextLabelRect(leftIndent, ++evadeLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_ExtendAngle"), contextLabel); + + GUI.Label(SettinglabelRect(leftIndent, ++evadeLines), $"{StringUtils.Localize("#LOC_BDArmory_AIWindow_ExtendTargetDist")}: {ActivePilot.extendTargetDist}m", Label); + if (!NumFieldsEnabled) + { + ActivePilot.extendTargetDist = GUI.HorizontalSlider(SettingSliderRect(leftIndent, evadeLines, contentWidth), ActivePilot.extendTargetDist, 0, 5000); + ActivePilot.extendTargetDist = BDAMath.RoundToUnit(ActivePilot.extendTargetDist, 25); + } + else + { + inputFields["extendTargetDist"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, evadeLines, contentWidth), inputFields["extendTargetDist"].possibleValue, 6, inputFieldStyle)); + ActivePilot.extendTargetDist = (float)inputFields["extendTargetDist"].currentValue; + } + if (contextTipsEnabled) GUI.Label(ContextLabelRect(leftIndent, ++evadeLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_ExtendDist"), contextLabel); + + GUI.Label(SettinglabelRect(leftIndent, ++evadeLines), $"{StringUtils.Localize("#LOC_BDArmory_AIWindow_ExtendAbortTime")}: {ActivePilot.extendAbortTime}s", Label); + if (!NumFieldsEnabled) + { + ActivePilot.extendAbortTime = GUI.HorizontalSlider(SettingSliderRect(leftIndent, evadeLines, contentWidth), ActivePilot.extendAbortTime, 5, 30); + ActivePilot.extendAbortTime = Mathf.Round(ActivePilot.extendAbortTime); + } + else + { + inputFields["extendAbortTime"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, evadeLines, contentWidth), inputFields["extendAbortTime"].possibleValue, 4, inputFieldStyle)); + ActivePilot.extendAbortTime = (float)inputFields["extendAbortTime"].currentValue; + } + if (contextTipsEnabled) GUI.Label(ContextLabelRect(leftIndent, ++evadeLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_ExtendAbortTimeContext"), contextLabel); + #endregion + } + ActivePilot.canExtend = GUI.Toggle(ToggleButtonRect(leftIndent, ++evadeLines, contentWidth), ActivePilot.canExtend, StringUtils.Localize("#LOC_BDArmory_ExtendToggle"), ActivePilot.canExtend ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Dynamic pid" + evadeLines += 1.25f; + #endregion + + GUI.EndGroup(); + evasionHeight = Mathf.Lerp(evasionHeight, evadeLines, 0.15f); + evadeLines += 0.1f; } - GUI.Label(SettinglabelRect(leftIndent, gndLines), Localizer.Format("#LOC_BDArmory_AIWindow_TurnRadiusMax") + ": " + ActivePilot.turnRadiusTwiddleFactorMax.ToString("0.0"), Label);//"dynamic damping min" - gndLines++; - if (contextTipsEnabled) + if (showTerrain) { - GUI.Label(ContextLabelRect(leftIndent, gndLines), Localizer.Format("#LOC_BDArmory_AIWindow_terrainMax"), contextLabel);//"dynamic damp min" - gndLines++; - } - GUI.EndGroup(); - terrainHeight = Mathf.Lerp(terrainHeight, gndLines, 0.15f); - gndLines += 0.1f; - } + gndLines += 0.2f; + GUI.BeginGroup( + new Rect(0, ((pidLines + altLines + spdLines + ctrlLines + evadeLines + gndLines) * entryHeight), contentWidth, terrainHeight * entryHeight), + GUIContent.none, BDArmorySetup.BDGuiSkin.box); + gndLines += 0.25f; - if (showRam) - { - ramLines += 0.2f; - GUI.BeginGroup( - new Rect(0, ((pidLines + altLines + spdLines + ctrlLines + evadeLines + gndLines + ramLines) * entryHeight), contentWidth, rammingHeight * entryHeight), - GUIContent.none, BDArmorySetup.BDGuiSkin.box); - ramLines += 0.25f; + GUI.Label(SettinglabelRect(leftIndent, gndLines), StringUtils.Localize("#LOC_BDArmory_PilotAI_Terrain"), BoldLabel);//"Speed" + + #region Terrain Avoidance Min + GUI.Label(SettinglabelRect(leftIndent, ++gndLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_TurnRadiusMin") + ": " + ActivePilot.turnRadiusTwiddleFactorMin.ToString("0.0"), Label); + var oldMinTwiddle = ActivePilot.turnRadiusTwiddleFactorMin; + if (!NumFieldsEnabled) + { + ActivePilot.turnRadiusTwiddleFactorMin = GUI.HorizontalSlider(SettingSliderRect(leftIndent, gndLines, contentWidth), ActivePilot.turnRadiusTwiddleFactorMin, 0.1f, ActivePilot.UpToEleven ? 10 : 5); + ActivePilot.turnRadiusTwiddleFactorMin = Mathf.Round(ActivePilot.turnRadiusTwiddleFactorMin * 10f) / 10f; + } + else + { + inputFields["turnRadiusTwiddleFactorMin"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, gndLines, contentWidth), inputFields["turnRadiusTwiddleFactorMin"].possibleValue, 6, inputFieldStyle)); + ActivePilot.turnRadiusTwiddleFactorMin = (float)inputFields["turnRadiusTwiddleFactorMin"].currentValue; + } + if (ActivePilot.turnRadiusTwiddleFactorMin != oldMinTwiddle) + { + ActivePilot.OnMinUpdated(null, null); + inputFields["turnRadiusTwiddleFactorMax"].currentValue = ActivePilot.turnRadiusTwiddleFactorMax; + } + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(leftIndent, ++gndLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_terrainMin"), contextLabel); + } + #endregion + + #region Terrain Avoidance Max + GUI.Label(SettinglabelRect(leftIndent, ++gndLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_TurnRadiusMax") + ": " + ActivePilot.turnRadiusTwiddleFactorMax.ToString("0.0"), Label); + var oldMaxTwiddle = ActivePilot.turnRadiusTwiddleFactorMax; + if (!NumFieldsEnabled) + { + ActivePilot.turnRadiusTwiddleFactorMax = GUI.HorizontalSlider(SettingSliderRect(leftIndent, gndLines, contentWidth), ActivePilot.turnRadiusTwiddleFactorMax, 0.1f, ActivePilot.UpToEleven ? 10 : 5); + ActivePilot.turnRadiusTwiddleFactorMax = Mathf.Round(ActivePilot.turnRadiusTwiddleFactorMax * 10) / 10; + } + else + { + inputFields["turnRadiusTwiddleFactorMax"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, gndLines, contentWidth), inputFields["turnRadiusTwiddleFactorMax"].possibleValue, 6, inputFieldStyle)); + ActivePilot.turnRadiusTwiddleFactorMax = (float)inputFields["turnRadiusTwiddleFactorMax"].currentValue; + } + if (ActivePilot.turnRadiusTwiddleFactorMax != oldMaxTwiddle) + { + ActivePilot.OnMaxUpdated(null, null); + inputFields["turnRadiusTwiddleFactorMin"].currentValue = ActivePilot.turnRadiusTwiddleFactorMin; + } + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(leftIndent, ++gndLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_terrainMax"), contextLabel); + } + #endregion - GUI.Label(SettinglabelRect(leftIndent, ramLines), Localizer.Format("#LOC_BDArmory_PilotAI_Ramming"), BoldLabel);//"Speed" - ramLines++; + #region Inverted Terrain Avoidance Critical Angle + GUI.Label(SettinglabelRect(leftIndent, ++gndLines), $"{StringUtils.Localize("#LOC_BDArmory_AIWindow_InvertedTerrainAvoidanceCriticalAngle")}: {ActivePilot.terrainAvoidanceCriticalAngle:0}", Label); + var oldTerrainAvoidanceCriticalAngle = ActivePilot.terrainAvoidanceCriticalAngle; + if (!NumFieldsEnabled) + { + ActivePilot.terrainAvoidanceCriticalAngle = GUI.HorizontalSlider(SettingSliderRect(leftIndent, gndLines, contentWidth), ActivePilot.terrainAvoidanceCriticalAngle, 90f, 180f); + ActivePilot.terrainAvoidanceCriticalAngle = Mathf.Round(ActivePilot.terrainAvoidanceCriticalAngle); + } + else + { + inputFields["terrainAvoidanceCriticalAngle"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, gndLines, contentWidth), inputFields["terrainAvoidanceCriticalAngle"].possibleValue, 6, inputFieldStyle)); + ActivePilot.terrainAvoidanceCriticalAngle = (float)inputFields["terrainAvoidanceCriticalAngle"].currentValue; + } + if (ActivePilot.terrainAvoidanceCriticalAngle != oldTerrainAvoidanceCriticalAngle) + { + ActivePilot.OnTerrainAvoidanceCriticalAngleChanged(null, null); + } + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(leftIndent, ++gndLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_InvertedTerrainAvoidanceCriticalAngleContext"), contextLabel); + } + #endregion - ActivePilot.allowRamming = GUI.Toggle(ToggleButtonRect(leftIndent, ramLines, contentWidth), - ActivePilot.allowRamming, Localizer.Format("#LOC_BDArmory_AllowRamming"), ActivePilot.allowRamming ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Dynamic pid" - ramLines += 1.25f; + #region Terrain Avoidance Control Surface Deployment Time + GUI.Label(SettinglabelRect(leftIndent, ++gndLines), $"{StringUtils.Localize("#LOC_BDArmory_AIWindow_TerrainAvoidanceVesselReactionTime")}: {ActivePilot.controlSurfaceDeploymentTime:0.0}", Label); + if (!NumFieldsEnabled) + { + ActivePilot.controlSurfaceDeploymentTime = GUI.HorizontalSlider(SettingSliderRect(leftIndent, gndLines, contentWidth), ActivePilot.controlSurfaceDeploymentTime, 0f, 4f); + ActivePilot.controlSurfaceDeploymentTime = BDAMath.RoundToUnit(ActivePilot.controlSurfaceDeploymentTime, 0.1f); + } + else + { + inputFields["controlSurfaceDeploymentTime"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, gndLines, contentWidth), inputFields["controlSurfaceDeploymentTime"].possibleValue, 6, inputFieldStyle)); + ActivePilot.controlSurfaceDeploymentTime = (float)inputFields["controlSurfaceDeploymentTime"].currentValue; + } + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(leftIndent, ++gndLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_TerrainAvoidanceVesselReactionTimeContext"), contextLabel); + } + #endregion - if (ActivePilot.allowRamming) - { + #region Waypoint Terrain Avoidance + GUI.Label(SettinglabelRect(leftIndent, ++gndLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_WaypointTerrainAvoidance") + ": " + ActivePilot.waypointTerrainAvoidance.ToString("0.00"), Label); if (!NumFieldsEnabled) { - ActivePilot.controlSurfaceLag = - GUI.HorizontalSlider(SettingSliderRect(leftIndent, ramLines, contentWidth), - ActivePilot.controlSurfaceLag, 0, ActivePilot.UpToEleven ? 1f : 0.2f); - ActivePilot.controlSurfaceLag = Mathf.Round(ActivePilot.controlSurfaceLag * 100) / 100; + ActivePilot.waypointTerrainAvoidance = GUI.HorizontalSlider(SettingSliderRect(leftIndent, gndLines, contentWidth), ActivePilot.waypointTerrainAvoidance, 0f, 1f); + ActivePilot.waypointTerrainAvoidance = BDAMath.RoundToUnit(ActivePilot.waypointTerrainAvoidance, 0.01f); } else { - inputFields["controlSurfaceLag"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, ramLines, contentWidth), inputFields["controlSurfaceLag"].possibleValue, 6)); - ActivePilot.controlSurfaceLag = (float)inputFields["controlSurfaceLag"].currentValue; + inputFields["waypointTerrainAvoidance"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, gndLines, contentWidth), inputFields["waypointTerrainAvoidance"].possibleValue, 6, inputFieldStyle)); + ActivePilot.waypointTerrainAvoidance = (float)inputFields["waypointTerrainAvoidance"].currentValue; } - GUI.Label(SettinglabelRect(leftIndent, ramLines), Localizer.Format("#LOC_BDArmory_AIWindow_ControlSurfaceLag") + ": " + ActivePilot.controlSurfaceLag.ToString("0.00"), Label);//"dynamic damping min" + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(leftIndent, ++gndLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_WaypointTerrainAvoidanceContext"), contextLabel); + } + #endregion + + ++gndLines; + GUI.EndGroup(); + terrainHeight = Mathf.Lerp(terrainHeight, gndLines, 0.15f); + gndLines += 0.1f; + } + + if (showRam) + { + ramLines += 0.2f; + GUI.BeginGroup( + new Rect(0, ((pidLines + altLines + spdLines + ctrlLines + evadeLines + gndLines + ramLines) * entryHeight), contentWidth, rammingHeight * entryHeight), + GUIContent.none, BDArmorySetup.BDGuiSkin.box); + ramLines += 0.25f; + GUI.Label(SettinglabelRect(leftIndent, ramLines), StringUtils.Localize("#LOC_BDArmory_PilotAI_Ramming"), BoldLabel);//"Speed" ramLines++; - if (contextTipsEnabled) + + ActivePilot.allowRamming = GUI.Toggle(ToggleButtonRect(leftIndent, ramLines, contentWidth), + ActivePilot.allowRamming, StringUtils.Localize("#LOC_BDArmory_AllowRamming"), ActivePilot.allowRamming ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Dynamic pid" + ramLines += 1.25f; + + if (ActivePilot.allowRamming) { - GUI.Label(ContextLabelRect(leftIndent, ramLines), Localizer.Format("#LOC_BDArmory_AIWindow_ramLag"), contextLabel);//"dynamic damp min" + if (!NumFieldsEnabled) + { + ActivePilot.controlSurfaceLag = + GUI.HorizontalSlider(SettingSliderRect(leftIndent, ramLines, contentWidth), + ActivePilot.controlSurfaceLag, 0, ActivePilot.UpToEleven ? 1f : 0.2f); + ActivePilot.controlSurfaceLag = Mathf.Round(ActivePilot.controlSurfaceLag * 100) / 100; + } + else + { + inputFields["controlSurfaceLag"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, ramLines, contentWidth), inputFields["controlSurfaceLag"].possibleValue, 6, inputFieldStyle)); + ActivePilot.controlSurfaceLag = (float)inputFields["controlSurfaceLag"].currentValue; + } + GUI.Label(SettinglabelRect(leftIndent, ramLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_ControlSurfaceLag") + ": " + ActivePilot.controlSurfaceLag.ToString("0.00"), Label); + ramLines++; + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(leftIndent, ramLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_ramLag"), contextLabel); + ramLines++; + } } + GUI.EndGroup(); + rammingHeight = Mathf.Lerp(rammingHeight, ramLines, 0.15f); + ramLines += 0.1f; } - GUI.EndGroup(); - rammingHeight = Mathf.Lerp(rammingHeight, ramLines, 0.15f); - ramLines += 0.1f; - } - if (showMisc) - { - miscLines += 0.2f; - GUI.BeginGroup( - new Rect(leftIndent, ((pidLines + altLines + spdLines + ctrlLines + evadeLines + gndLines + ramLines + miscLines) * entryHeight), contentWidth, miscHeight * entryHeight), - GUIContent.none, BDArmorySetup.BDGuiSkin.box); - miscLines += 0.25f; - - GUI.Label(SettinglabelRect(leftIndent, miscLines), Localizer.Format("#LOC_BDArmory_Orbit"), BoldLabel);//"Speed" - miscLines++; - - ActivePilot.ClockwiseOrbit = GUI.Toggle(ToggleButtonRect(leftIndent, miscLines, contentWidth), - ActivePilot.ClockwiseOrbit, ActivePilot.ClockwiseOrbit ? Localizer.Format("#LOC_BDArmory_Orbit_Starboard") : Localizer.Format("#LOC_BDArmory_Orbit_Port"), ActivePilot.ClockwiseOrbit ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Dynamic pid" - miscLines += 1.25f; - if (contextTipsEnabled) + if (showMisc) { - GUI.Label(ContextLabelRect(leftIndent, miscLines), Localizer.Format("#LOC_BDArmory_AIWindow_orbit"), Label);//"dynamic damp min" + miscLines += 0.2f; + GUI.BeginGroup( + new Rect(0, ((pidLines + altLines + spdLines + ctrlLines + evadeLines + gndLines + ramLines + miscLines) * entryHeight), contentWidth, miscHeight * entryHeight), + GUIContent.none, BDArmorySetup.BDGuiSkin.box); + miscLines += 0.25f; + + GUI.Label(SettinglabelRect(leftIndent, miscLines), StringUtils.Localize("#LOC_BDArmory_Orbit"), BoldLabel);//"orbit" miscLines++; - } - GUI.Label(SettinglabelRect(leftIndent, miscLines), Localizer.Format("#LOC_BDArmory_StandbyMode"), BoldLabel);//"Speed" - miscLines++; + ActivePilot.ClockwiseOrbit = GUI.Toggle(ToggleButtonRect(leftIndent, miscLines, contentWidth), + ActivePilot.ClockwiseOrbit, ActivePilot.ClockwiseOrbit ? StringUtils.Localize("#LOC_BDArmory_Orbit_Starboard") : StringUtils.Localize("#LOC_BDArmory_Orbit_Port"), ActivePilot.ClockwiseOrbit ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Dynamic pid" + miscLines += 1.25f; + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(leftIndent, miscLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_orbit"), Label);//"orbit direction" + miscLines++; + } - ActivePilot.standbyMode = GUI.Toggle(ToggleButtonRect(leftIndent, miscLines, contentWidth), - ActivePilot.standbyMode, ActivePilot.standbyMode ? Localizer.Format("#LOC_BDArmory_On") : Localizer.Format("#LOC_BDArmory_Off"), ActivePilot.standbyMode ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Dynamic pid" - miscLines += 1.25f; - if (contextTipsEnabled) - { - GUI.Label(ContextLabelRect(leftIndent, miscLines), Localizer.Format("#LOC_BDArmory_AIWindow_standby"), Label);//"dynamic damp min" + GUI.Label(SettinglabelRect(leftIndent, miscLines), StringUtils.Localize("#LOC_BDArmory_StandbyMode"), BoldLabel);//"Standby" miscLines++; - } + ActivePilot.standbyMode = GUI.Toggle(ToggleButtonRect(leftIndent, miscLines, contentWidth), + ActivePilot.standbyMode, ActivePilot.standbyMode ? StringUtils.Localize("#LOC_BDArmory_On") : StringUtils.Localize("#LOC_BDArmory_Off"), ActivePilot.standbyMode ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Dynamic pid" + miscLines += 1.25f; + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(leftIndent, miscLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_standby"), Label);//"Activate when target in guard range" + miscLines++; + } + + GUI.Label(SettinglabelRect(leftIndent, miscLines), StringUtils.Localize("#LOC_BDArmory_ControlSurfaceSettings"), BoldLabel);//"Control Surface Settings" + miscLines++; + + if (GUI.Button(ToggleButtonRect(leftIndent, miscLines, contentWidth), StringUtils.Localize("#LOC_BDArmory_StoreControlSurfaceSettings"), BDArmorySetup.BDGuiSkin.button)) + { + ActivePilot.StoreControlSurfaceSettings(); //Hiding these in misc is probably not the best place to put them, but only so much space on the window header bar + } + miscLines += 1.25f; + if (ActivePilot.Events["RestoreControlSurfaceSettings"].active == true) + { + GUIStyle restoreStyle = BDArmorySetup.BDGuiSkin.button; + if (GUI.Button(ToggleButtonRect(leftIndent, miscLines, contentWidth), StringUtils.Localize("#LOC_BDArmory_RestoreControlSurfaceSettings"), restoreStyle)) + { + ActivePilot.RestoreControlSurfaceSettings(); + } + miscLines += 1.25f; + } + + GUI.EndGroup(); + miscHeight = Mathf.Lerp(miscHeight, miscLines, 0.15f); + miscLines += 0.1f; + } + contentHeight = (pidLines + altLines + spdLines + ctrlLines + evadeLines + gndLines + ramLines + miscLines) * entryHeight; GUI.EndGroup(); - miscHeight = Mathf.Lerp(miscHeight, miscLines, 0.15f); - miscLines += 0.1f; + GUI.EndScrollView(); } - height = Mathf.Max(Mathf.Lerp(height, (pidLines + altLines + spdLines + ctrlLines + evadeLines + gndLines + ramLines + miscLines) * entryHeight + contentTop + 5, 1), (line * entryHeight) + contentTop); - GUI.EndGroup(); - GUI.EndScrollView(); + else contentHeight = 0; } - else + else if (ActiveDriver != null) { line++; ActiveDriver.UpToEleven = GUI.Toggle(SubsectionRect(leftIndent, line), - ActiveDriver.UpToEleven, ActiveDriver.UpToEleven ? Localizer.Format("#LOC_BDArmory_UnclampTuning_enabledText") : Localizer.Format("#LOC_BDArmory_UnclampTuning_disabledText"), ActiveDriver.UpToEleven ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Misc" + ActiveDriver.UpToEleven, ActiveDriver.UpToEleven ? StringUtils.Localize("#LOC_BDArmory_UnclampTuning_enabledText") : StringUtils.Localize("#LOC_BDArmory_UnclampTuning_disabledText"), ActiveDriver.UpToEleven ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Misc" if (ActiveDriver.UpToEleven != oldClamp) { SetInputFields(ActiveDriver.GetType()); @@ -2053,42 +2286,42 @@ void WindowRectAI(int windowID) float driverLines = 0; - if (height < WindowHeight) - { - height = WindowHeight - (entryHeight * 1.5f); - } - if (infoLinkEnabled) { windowColumns = 3; - GUI.Label(new Rect(leftIndent + (ColumnWidth * 2), (contentTop), ColumnWidth - (leftIndent), entryHeight), Localizer.Format("#LOC_BDArmory_AIWindow_infoLink"), Title);//"infolink" - BeginArea(new Rect(leftIndent + (ColumnWidth * 2), contentTop + (entryHeight * 1.5f), ColumnWidth - (leftIndent), WindowHeight - (entryHeight * 1.5f) - (2 * contentTop))); - using (var scrollViewScope = new ScrollViewScope(scrollInfoVector, Width(ColumnWidth - (leftIndent)), Height(WindowHeight - (entryHeight * 1.5f) - (2 * contentTop)))) + GUI.Label(new Rect(leftIndent + ColumnWidth * 2, contentTop, ColumnWidth - leftIndent, entryHeight), StringUtils.Localize("#LOC_BDArmory_AIWindow_infoLink"), Title);//"infolink" + BeginArea(new Rect(leftIndent + ColumnWidth * 2, contentTop + entryHeight * 1.5f, ColumnWidth - leftIndent, WindowHeight - entryHeight * 1.5f - 2 * contentTop)); + using (var scrollViewScope = new ScrollViewScope(scrollInfoVector, Width(ColumnWidth - leftIndent), Height(WindowHeight - entryHeight * 1.5f - 2 * contentTop))) { scrollInfoVector = scrollViewScope.scrollPosition; - GUILayout.Label(Localizer.Format("#LOC_BDArmory_DriverAI_Help"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //Pid desc - GUILayout.Label(Localizer.Format("#LOC_BDArmory_DriverAI_Slopes"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //tgt pitch, slope angle desc - GUILayout.Label(Localizer.Format("#LOC_BDArmory_DriverAI_Speeds"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //cruise, flank speed desc - GUILayout.Label(Localizer.Format("#LOC_BDArmory_DriverAI_Drift"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //drift angle desc - GUILayout.Label(Localizer.Format("#LOC_BDArmory_DriverAI_bank"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //bank angle desc - GUILayout.Label(Localizer.Format("#LOC_BDArmory_DriverAI_steerMult"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //steer mult desc - GUILayout.Label(Localizer.Format("#LOC_BDArmory_DriverAI_SteerDamp"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //steer damp desc - GUILayout.Label(Localizer.Format("#LOC_BDArmory_DriverAI_Orientation"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //attack vector, broadside desc - GUILayout.Label(Localizer.Format("#LOC_BDArmory_DriverAI_Engagement"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //engage ranges desc - GUILayout.Label(Localizer.Format("#LOC_BDArmory_DriverAI_RCS"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //RCS desc - GUILayout.Label(Localizer.Format("#LOC_BDArmory_DriverAI_Mass"), infoLinkStyle, Width(ColumnWidth - (leftIndent * 4) - 20)); //avoid mass desc + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_DriverAI_Help"), infoLinkStyle, Width(ColumnWidth - leftIndent * 4 - 20)); //Pid desc + if (ActiveDriver.SurfaceType != AIUtils.VehicleMovementType.Stationary) + { + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_DriverAI_Slopes"), infoLinkStyle, Width(ColumnWidth - leftIndent * 4 - 20)); //tgt pitch, slope angle desc + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_DriverAI_Speeds"), infoLinkStyle, Width(ColumnWidth - leftIndent * 4 - 20)); //cruise, flank speed desc + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_DriverAI_Drift"), infoLinkStyle, Width(ColumnWidth - leftIndent * 4 - 20)); //drift angle desc + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_DriverAI_bank"), infoLinkStyle, Width(ColumnWidth - leftIndent * 4 - 20)); //bank angle desc + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_DriverAI_steerMult"), infoLinkStyle, Width(ColumnWidth - leftIndent * 4 - 20)); //steer mult desc + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_DriverAI_SteerDamp"), infoLinkStyle, Width(ColumnWidth - leftIndent * 4 - 20)); //steer damp desc + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_DriverAI_Orientation"), infoLinkStyle, Width(ColumnWidth - leftIndent * 4 - 20)); //attack vector, broadside desc + } + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_DriverAI_Engagement"), infoLinkStyle, Width(ColumnWidth - leftIndent * 4 - 20)); //engage ranges desc + if (ActiveDriver.SurfaceType != AIUtils.VehicleMovementType.Stationary) + { + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_DriverAI_RCS"), infoLinkStyle, Width(ColumnWidth - leftIndent * 4 - 20)); //RCS desc + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_DriverAI_Mass"), infoLinkStyle, Width(ColumnWidth - leftIndent * 4 - 20)); //avoid mass desc + } } EndArea(); } - scrollViewSAIVector = GUI.BeginScrollView(new Rect(leftIndent + 100, contentTop + (entryHeight * 1.5f), (ColumnWidth * 2) - 100 - (leftIndent), WindowHeight - (entryHeight * 1.5f) - (2 * contentTop)), scrollViewSAIVector, - new Rect(0, 0, (ColumnWidth * 2) - 120 - (leftIndent * 2), height + contentTop)); + scrollViewSAIVector = GUI.BeginScrollView(new Rect(leftIndent + 100, contentTop + entryHeight * 1.5f, (ColumnWidth * 2) - 100 - leftIndent, WindowHeight - entryHeight * 1.5f - 2 * contentTop), scrollViewSAIVector, + new Rect(0, 0, ColumnWidth * 2 - 120 - leftIndent * 2, height + contentTop)); - GUI.BeginGroup(new Rect(leftIndent, 0, (ColumnWidth * 2) - 120 - (leftIndent * 2), height), GUIContent.none, BDArmorySetup.BDGuiSkin.box); //darker box + GUI.BeginGroup(new Rect(leftIndent, 0, ColumnWidth * 2 - 120 - leftIndent * 2, height + 2 * contentTop), GUIContent.none, BDArmorySetup.BDGuiSkin.box); //darker box - //GUI.Box(new Rect(0, 0, (ColumnWidth * 2) - leftIndent, height - contentTop), "", BDArmorySetup.BDGuiSkin.window); contentWidth -= 24; leftIndent += 3; @@ -2103,134 +2336,138 @@ void WindowRectAI(int windowID) ActiveDriver.SurfaceTypeName = VehicleMovementTypes[Drivertype].ToString(); ActiveDriver.ChooseOptionsUpdated(null, null); } - GUI.Label(SettinglabelRect(leftIndent, driverLines), Localizer.Format("#LOC_BDArmory_VehicleType") + ": " + ActiveDriver.SurfaceTypeName, Label);//"Wobbly" + GUI.Label(SettinglabelRect(leftIndent, driverLines), StringUtils.Localize("#LOC_BDArmory_VehicleType") + ": " + ActiveDriver.SurfaceTypeName, Label);//"Wobbly" driverLines++; if (contextTipsEnabled) { - GUI.Label(ContextLabelRect(leftIndent, driverLines), Localizer.Format("#LOC_BDArmory_DriverAI_VeeType"), contextLabel);//"Wobbly" + GUI.Label(ContextLabelRect(leftIndent, driverLines), StringUtils.Localize("#LOC_BDArmory_DriverAI_VeeType"), contextLabel);//"Wobbly" driverLines++; } - if (!NumFieldsEnabled) - { - ActiveDriver.MaxSlopeAngle = - GUI.HorizontalSlider(SettingSliderRect(leftIndent, driverLines, contentWidth), - ActiveDriver.MaxSlopeAngle, 10, ActiveDriver.UpToEleven ? 90 : 30); - ActiveDriver.MaxSlopeAngle = Mathf.Round(ActiveDriver.MaxSlopeAngle); - } - else - { - inputFields["MaxSlopeAngle"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, driverLines, contentWidth), inputFields["MaxSlopeAngle"].possibleValue, 6)); - ActiveDriver.MaxSlopeAngle = (float)inputFields["MaxSlopeAngle"].currentValue; - } - GUI.Label(SettinglabelRect(leftIndent, driverLines), Localizer.Format("#LOC_BDArmory_MaxSlopeAngle") + ": " + ActiveDriver.MaxSlopeAngle.ToString("0"), Label);//"Steer Ki" - driverLines++; - if (contextTipsEnabled) - { - GUI.Label(ContextLabelRect(leftIndent, driverLines), Localizer.Format("#LOC_BDArmory_DriverAI_SlopeAngle"), contextLabel);//"undershoot" - driverLines++; - } - if (!NumFieldsEnabled) + if (ActiveDriver.SurfaceType != AIUtils.VehicleMovementType.Stationary) { - ActiveDriver.CruiseSpeed = + if (!NumFieldsEnabled) + { + ActiveDriver.MaxSlopeAngle = GUI.HorizontalSlider(SettingSliderRect(leftIndent, driverLines, contentWidth), - ActiveDriver.CruiseSpeed, 5, ActiveDriver.UpToEleven ? 300 : 60); - ActiveDriver.CruiseSpeed = Mathf.Round(ActiveDriver.CruiseSpeed); - } - else - { - inputFields["CruiseSpeed"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, driverLines, contentWidth), inputFields["CruiseSpeed"].possibleValue, 6)); - ActiveDriver.CruiseSpeed = (float)inputFields["CruiseSpeed"].currentValue; - } - GUI.Label(SettinglabelRect(leftIndent, driverLines), Localizer.Format("#LOC_BDArmory_CruiseSpeed") + ": " + ActiveDriver.CruiseSpeed.ToString("0"), Label);//"Steer Damping" - - driverLines++; - if (contextTipsEnabled) - { - GUI.Label(ContextLabelRect(leftIndent, driverLines), Localizer.Format("#LOC_BDArmory_AIWindow_idleSpeed"), contextLabel);//"Wobbly" + ActiveDriver.MaxSlopeAngle, 10, ActiveDriver.UpToEleven ? 90 : 30); + ActiveDriver.MaxSlopeAngle = Mathf.Round(ActiveDriver.MaxSlopeAngle); + } + else + { + inputFields["MaxSlopeAngle"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, driverLines, contentWidth), inputFields["MaxSlopeAngle"].possibleValue, 6, inputFieldStyle)); + ActiveDriver.MaxSlopeAngle = (float)inputFields["MaxSlopeAngle"].currentValue; + } + GUI.Label(SettinglabelRect(leftIndent, driverLines), StringUtils.Localize("#LOC_BDArmory_MaxSlopeAngle") + ": " + ActiveDriver.MaxSlopeAngle.ToString("0"), Label);//"Steer Ki" driverLines++; - } - if (!NumFieldsEnabled) - { - ActiveDriver.MaxSpeed = - GUI.HorizontalSlider(SettingSliderRect(leftIndent, driverLines, contentWidth), - ActiveDriver.MaxSpeed, 5, ActiveDriver.UpToEleven ? 400 : 80); - ActiveDriver.MaxSpeed = Mathf.Round(ActiveDriver.MaxSpeed); - } - else - { - inputFields["MaxSpeed"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, driverLines, contentWidth), inputFields["MaxSpeed"].possibleValue, 6)); - ActiveDriver.MaxSpeed = (float)inputFields["MaxSpeed"].currentValue; - } - GUI.Label(SettinglabelRect(leftIndent, driverLines), Localizer.Format("#LOC_BDArmory_MaxSpeed") + ": " + ActiveDriver.MaxSpeed.ToString("0"), Label);//"Steer Damping" + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(leftIndent, driverLines), StringUtils.Localize("#LOC_BDArmory_DriverAI_SlopeAngle"), contextLabel);//"undershoot" + driverLines++; + } - driverLines++; - if (contextTipsEnabled) - { - GUI.Label(ContextLabelRect(leftIndent, driverLines), Localizer.Format("#LOC_BDArmory_DriverAI_MaxSpeed"), contextLabel);//"Wobbly" + if (!NumFieldsEnabled) + { + ActiveDriver.CruiseSpeed = + GUI.HorizontalSlider(SettingSliderRect(leftIndent, driverLines, contentWidth), + ActiveDriver.CruiseSpeed, 5, ActiveDriver.UpToEleven ? 300 : 60); + ActiveDriver.CruiseSpeed = Mathf.Round(ActiveDriver.CruiseSpeed); + } + else + { + inputFields["CruiseSpeed"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, driverLines, contentWidth), inputFields["CruiseSpeed"].possibleValue, 6, inputFieldStyle)); + ActiveDriver.CruiseSpeed = (float)inputFields["CruiseSpeed"].currentValue; + } + GUI.Label(SettinglabelRect(leftIndent, driverLines), StringUtils.Localize("#LOC_BDArmory_CruiseSpeed") + ": " + ActiveDriver.CruiseSpeed.ToString("0"), Label);//"Steer Damping" driverLines++; - } - if (!NumFieldsEnabled) - { - ActiveDriver.MaxDrift = - GUI.HorizontalSlider(SettingSliderRect(leftIndent, driverLines, contentWidth), - ActiveDriver.MaxDrift, 1, 180); - ActiveDriver.MaxDrift = Mathf.Round(ActiveDriver.MaxDrift); - } - else - { - inputFields["MaxDrift"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, driverLines, contentWidth), inputFields["MaxDrift"].possibleValue, 6)); - ActiveDriver.MaxDrift = (float)inputFields["MaxDrift"].currentValue; - } - GUI.Label(SettinglabelRect(leftIndent, driverLines), Localizer.Format("#LOC_BDArmory_MaxDrift") + ": " + ActiveDriver.MaxDrift.ToString("0"), Label);//"Steer Damping" + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(leftIndent, driverLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_idleSpeed"), contextLabel);//"Wobbly" + driverLines++; + } - driverLines++; - if (contextTipsEnabled) - { - GUI.Label(ContextLabelRect(leftIndent, driverLines), Localizer.Format("#LOC_BDArmory_DriverAI_MaxDrift"), contextLabel);//"Wobbly" + if (!NumFieldsEnabled) + { + ActiveDriver.MaxSpeed = + GUI.HorizontalSlider(SettingSliderRect(leftIndent, driverLines, contentWidth), + ActiveDriver.MaxSpeed, 5, ActiveDriver.UpToEleven ? 400 : 80); + ActiveDriver.MaxSpeed = Mathf.Round(ActiveDriver.MaxSpeed); + } + else + { + inputFields["MaxSpeed"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, driverLines, contentWidth), inputFields["MaxSpeed"].possibleValue, 6, inputFieldStyle)); + ActiveDriver.MaxSpeed = (float)inputFields["MaxSpeed"].currentValue; + } + GUI.Label(SettinglabelRect(leftIndent, driverLines), StringUtils.Localize("#LOC_BDArmory_MaxSpeed") + ": " + ActiveDriver.MaxSpeed.ToString("0"), Label);//"Steer Damping" driverLines++; - } - if (!NumFieldsEnabled) - { - ActiveDriver.TargetPitch = + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(leftIndent, driverLines), StringUtils.Localize("#LOC_BDArmory_DriverAI_MaxSpeed"), contextLabel);//"Wobbly" + driverLines++; + } + + if (!NumFieldsEnabled) + { + ActiveDriver.MaxDrift = GUI.HorizontalSlider(SettingSliderRect(leftIndent, driverLines, contentWidth), - ActiveDriver.TargetPitch, -10, 10); - ActiveDriver.TargetPitch = Mathf.Round(ActiveDriver.TargetPitch * 10) / 10; - } - else - { - inputFields["TargetPitch"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, driverLines, contentWidth), inputFields["TargetPitch"].possibleValue, 6)); - ActiveDriver.TargetPitch = (float)inputFields["TargetPitch"].currentValue; - } - GUI.Label(SettinglabelRect(leftIndent, driverLines), Localizer.Format("#LOC_BDArmory_TargetPitch") + ": " + ActiveDriver.TargetPitch.ToString("0.0"), Label);//"Steer Damping" + ActiveDriver.MaxDrift, 1, 180); + ActiveDriver.MaxDrift = Mathf.Round(ActiveDriver.MaxDrift); + } + else + { + inputFields["MaxDrift"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, driverLines, contentWidth), inputFields["MaxDrift"].possibleValue, 6, inputFieldStyle)); + ActiveDriver.MaxDrift = (float)inputFields["MaxDrift"].currentValue; + } + GUI.Label(SettinglabelRect(leftIndent, driverLines), StringUtils.Localize("#LOC_BDArmory_MaxDrift") + ": " + ActiveDriver.MaxDrift.ToString("0"), Label);//"Steer Damping" + driverLines++; + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(leftIndent, driverLines), StringUtils.Localize("#LOC_BDArmory_DriverAI_MaxDrift"), contextLabel);//"Wobbly" + driverLines++; + } - driverLines++; - if (contextTipsEnabled) - { - GUI.Label(ContextLabelRect(leftIndent, driverLines), Localizer.Format("#LOC_BDArmory_DriverAI_Pitch"), contextLabel);//"Wobbly" + if (!NumFieldsEnabled) + { + ActiveDriver.TargetPitch = + GUI.HorizontalSlider(SettingSliderRect(leftIndent, driverLines, contentWidth), + ActiveDriver.TargetPitch, -10, 10); + ActiveDriver.TargetPitch = Mathf.Round(ActiveDriver.TargetPitch * 10) / 10; + } + else + { + inputFields["TargetPitch"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, driverLines, contentWidth), inputFields["TargetPitch"].possibleValue, 6, inputFieldStyle)); + ActiveDriver.TargetPitch = (float)inputFields["TargetPitch"].currentValue; + } + GUI.Label(SettinglabelRect(leftIndent, driverLines), StringUtils.Localize("#LOC_BDArmory_TargetPitch") + ": " + ActiveDriver.TargetPitch.ToString("0.0"), Label);//"Steer Damping" driverLines++; - } - if (!NumFieldsEnabled) - { - ActiveDriver.BankAngle = - GUI.HorizontalSlider(SettingSliderRect(leftIndent, driverLines, contentWidth), - ActiveDriver.BankAngle, -45, 45); - ActiveDriver.BankAngle = Mathf.Round(ActiveDriver.BankAngle); - } - else - { - inputFields["BankAngle"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, driverLines, contentWidth), inputFields["BankAngle"].possibleValue, 6)); - ActiveDriver.BankAngle = (float)inputFields["BankAngle"].currentValue; - } - GUI.Label(SettinglabelRect(leftIndent, driverLines), Localizer.Format("#LOC_BDArmory_BankAngle") + ": " + ActiveDriver.BankAngle.ToString("0"), Label);//"Steer Damping" + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(leftIndent, driverLines), StringUtils.Localize("#LOC_BDArmory_DriverAI_Pitch"), contextLabel);//"Wobbly" + driverLines++; + } - driverLines++; - if (contextTipsEnabled) - { - GUI.Label(ContextLabelRect(leftIndent, driverLines), Localizer.Format("#LOC_BDArmory_AIWindow_bankLimit"), contextLabel);//"Wobbly" + if (!NumFieldsEnabled) + { + ActiveDriver.BankAngle = + GUI.HorizontalSlider(SettingSliderRect(leftIndent, driverLines, contentWidth), + ActiveDriver.BankAngle, -45, 45); + ActiveDriver.BankAngle = Mathf.Round(ActiveDriver.BankAngle); + } + else + { + inputFields["BankAngle"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, driverLines, contentWidth), inputFields["BankAngle"].possibleValue, 6, inputFieldStyle)); + ActiveDriver.BankAngle = (float)inputFields["BankAngle"].currentValue; + } + GUI.Label(SettinglabelRect(leftIndent, driverLines), StringUtils.Localize("#LOC_BDArmory_BankAngle") + ": " + ActiveDriver.BankAngle.ToString("0"), Label);//"Steer Damping" driverLines++; + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(leftIndent, driverLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_bankLimit"), contextLabel);//"Wobbly" + driverLines++; + } } + if (!NumFieldsEnabled) { ActiveDriver.steerMult = @@ -2240,17 +2477,17 @@ void WindowRectAI(int windowID) } else { - inputFields["steerMult"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, driverLines, contentWidth), inputFields["steerMult"].possibleValue, 6)); + inputFields["steerMult"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, driverLines, contentWidth), inputFields["steerMult"].possibleValue, 6, inputFieldStyle)); ActiveDriver.steerMult = (float)inputFields["steerMult"].currentValue; } - GUI.Label(SettinglabelRect(leftIndent, driverLines), Localizer.Format("#LOC_BDArmory_SteerFactor") + ": " + ActiveDriver.steerMult.ToString("0.0"), Label);//"Steer Damping" - + GUI.Label(SettinglabelRect(leftIndent, driverLines), StringUtils.Localize("#LOC_BDArmory_SteerFactor") + ": " + ActiveDriver.steerMult.ToString("0.0"), Label);//"Steer Damping" driverLines++; if (contextTipsEnabled) { - GUI.Label(ContextLabelRect(leftIndent, driverLines), Localizer.Format("#LOC_BDArmory_DriverAI_SteerMult"), contextLabel);//"Wobbly" + GUI.Label(ContextLabelRect(leftIndent, driverLines), StringUtils.Localize("#LOC_BDArmory_DriverAI_SteerMult"), contextLabel);//"Wobbly" driverLines++; } + if (!NumFieldsEnabled) { ActiveDriver.steerDamping = @@ -2260,26 +2497,29 @@ void WindowRectAI(int windowID) } else { - inputFields["steerDamping"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, driverLines, contentWidth), inputFields["steerDamping"].possibleValue, 6)); + inputFields["steerDamping"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, driverLines, contentWidth), inputFields["steerDamping"].possibleValue, 6, inputFieldStyle)); ActiveDriver.steerDamping = (float)inputFields["steerDamping"].currentValue; } - GUI.Label(SettinglabelRect(leftIndent, driverLines), Localizer.Format("#LOC_BDArmory_SteerDamping") + ": " + ActiveDriver.steerDamping.ToString("0.0"), Label);//"Steer Damping" - + GUI.Label(SettinglabelRect(leftIndent, driverLines), StringUtils.Localize("#LOC_BDArmory_SteerDamping") + ": " + ActiveDriver.steerDamping.ToString("0.0"), Label);//"Steer Damping" driverLines++; if (contextTipsEnabled) { - GUI.Label(ContextLabelRect(leftIndent, driverLines), Localizer.Format("#LOC_BDArmory_AIWindow_DynDampMult"), contextLabel);//"Wobbly" + GUI.Label(ContextLabelRect(leftIndent, driverLines), StringUtils.Localize("#LOC_BDArmory_AIWindow_DynDampMult"), contextLabel);//"Wobbly" driverLines++; } - ActiveDriver.BroadsideAttack = GUI.Toggle(ToggleButtonRect(leftIndent, driverLines, contentWidth), - ActiveDriver.BroadsideAttack, Localizer.Format("#LOC_BDArmory_BroadsideAttack") + " : " + (ActiveDriver.BroadsideAttack ? Localizer.Format("#LOC_BDArmory_BroadsideAttack_enabledText") : Localizer.Format("#LOC_BDArmory_BroadsideAttack_disabledText")), ActiveDriver.BroadsideAttack ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Dynamic pid" - driverLines += 1.25f; - if (contextTipsEnabled) + if (ActiveDriver.SurfaceType != AIUtils.VehicleMovementType.Stationary) { - GUI.Label(ContextLabelRect(leftIndent, driverLines), Localizer.Format("#LOC_BDArmory_DriverAI_AtkVector"), contextLabel);//"dynamic damp min" - driverLines++; + ActiveDriver.BroadsideAttack = GUI.Toggle(ToggleButtonRect(leftIndent, driverLines, contentWidth), + ActiveDriver.BroadsideAttack, StringUtils.Localize("#LOC_BDArmory_BroadsideAttack") + " : " + (ActiveDriver.BroadsideAttack ? StringUtils.Localize("#LOC_BDArmory_BroadsideAttack_enabledText") : StringUtils.Localize("#LOC_BDArmory_BroadsideAttack_disabledText")), ActiveDriver.BroadsideAttack ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Dynamic pid" + driverLines += 1.25f; + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(leftIndent, driverLines), StringUtils.Localize("#LOC_BDArmory_DriverAI_AtkVector"), contextLabel); + driverLines++; + } } + if (!NumFieldsEnabled) { ActiveDriver.MinEngagementRange = @@ -2289,17 +2529,17 @@ void WindowRectAI(int windowID) } else { - inputFields["MinEngagementRange"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, driverLines, contentWidth), inputFields["MinEngagementRange"].possibleValue, 6)); + inputFields["MinEngagementRange"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, driverLines, contentWidth), inputFields["MinEngagementRange"].possibleValue, 6, inputFieldStyle)); ActiveDriver.MinEngagementRange = (float)inputFields["MinEngagementRange"].currentValue; } - GUI.Label(SettinglabelRect(leftIndent, driverLines), Localizer.Format("#LOC_BDArmory_EngageRangeMin") + ": " + ActiveDriver.MinEngagementRange.ToString("0"), Label);//"Steer Damping" - + GUI.Label(SettinglabelRect(leftIndent, driverLines), StringUtils.Localize("#LOC_BDArmory_EngageRangeMin") + ": " + ActiveDriver.MinEngagementRange.ToString("0"), Label);//"Steer Damping" driverLines++; if (contextTipsEnabled) { - GUI.Label(ContextLabelRect(leftIndent, driverLines), Localizer.Format("#LOC_BDArmory_DriverAI_MinEngage"), contextLabel);//"Wobbly" + GUI.Label(ContextLabelRect(leftIndent, driverLines), StringUtils.Localize("#LOC_BDArmory_DriverAI_MinEngage"), contextLabel);//"Wobbly" driverLines++; } + if (!NumFieldsEnabled) { ActiveDriver.MaxEngagementRange = @@ -2309,78 +2549,91 @@ void WindowRectAI(int windowID) } else { - inputFields["MaxEngagementRange"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, driverLines, contentWidth), inputFields["MaxEngagementRange"].possibleValue, 6)); + inputFields["MaxEngagementRange"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, driverLines, contentWidth), inputFields["MaxEngagementRange"].possibleValue, 6, inputFieldStyle)); ActiveDriver.MaxEngagementRange = (float)inputFields["MaxEngagementRange"].currentValue; } - GUI.Label(SettinglabelRect(leftIndent, driverLines), Localizer.Format("#LOC_BDArmory_EngageRangeMax") + ": " + ActiveDriver.MaxEngagementRange.ToString("0"), Label);//"Steer Damping" - + GUI.Label(SettinglabelRect(leftIndent, driverLines), StringUtils.Localize("#LOC_BDArmory_EngageRangeMax") + ": " + ActiveDriver.MaxEngagementRange.ToString("0"), Label);//"Steer Damping" driverLines++; if (contextTipsEnabled) { - GUI.Label(ContextLabelRect(leftIndent, driverLines), Localizer.Format("#LOC_BDArmory_DriverAI_MaxEngage"), contextLabel);//"Wobbly" + GUI.Label(ContextLabelRect(leftIndent, driverLines), StringUtils.Localize("#LOC_BDArmory_DriverAI_MaxEngage"), contextLabel);//"Wobbly" driverLines++; } ActiveDriver.ManeuverRCS = GUI.Toggle(ToggleButtonRect(leftIndent, driverLines, contentWidth), - ActiveDriver.ManeuverRCS, Localizer.Format("#LOC_BDArmory_ManeuverRCS") + " : " + (ActiveDriver.ManeuverRCS ? Localizer.Format("#LOC_BDArmory_ManeuverRCS_enabledText") : Localizer.Format("#LOC_BDArmory_ManeuverRCS_disabledText")), ActiveDriver.BroadsideAttack ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Dynamic pid" + ActiveDriver.ManeuverRCS, StringUtils.Localize("#LOC_BDArmory_ManeuverRCS") + " : " + (ActiveDriver.ManeuverRCS ? StringUtils.Localize("#LOC_BDArmory_ManeuverRCS_enabledText") : StringUtils.Localize("#LOC_BDArmory_ManeuverRCS_disabledText")), ActiveDriver.BroadsideAttack ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button);//"Dynamic pid" driverLines += 1.25f; if (contextTipsEnabled) { - GUI.Label(ContextLabelRect(leftIndent, driverLines), Localizer.Format("#LOC_BDArmory_DriverAI_RCS"), contextLabel);//"dynamic damp min" + GUI.Label(ContextLabelRect(leftIndent, driverLines), StringUtils.Localize("#LOC_BDArmory_DriverAI_RCS"), contextLabel); driverLines++; } - if (!NumFieldsEnabled) - { - ActiveDriver.AvoidMass = - GUI.HorizontalSlider(SettingSliderRect(leftIndent, driverLines, contentWidth), - ActiveDriver.AvoidMass, 0, ActiveDriver.UpToEleven ? 1000000f : 100); - ActiveDriver.AvoidMass = Mathf.Round(ActiveDriver.AvoidMass); - } - else - { - inputFields["AvoidMass"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, driverLines, contentWidth), inputFields["AvoidMass"].possibleValue, 7)); - ActiveDriver.AvoidMass = (float)inputFields["AvoidMass"].currentValue; - } - GUI.Label(SettinglabelRect(leftIndent, driverLines), Localizer.Format("#LOC_BDArmory_MinObstacleMass") + ": " + ActiveDriver.AvoidMass.ToString("0"), Label);//"Steer Damping" - - driverLines++; - if (contextTipsEnabled) + if (ActiveDriver.SurfaceType != AIUtils.VehicleMovementType.Stationary) { - GUI.Label(ContextLabelRect(leftIndent, driverLines), Localizer.Format("#LOC_BDArmory_DriverAI_Mass"), contextLabel);//"Wobbly" + if (!NumFieldsEnabled) + { + ActiveDriver.AvoidMass = + GUI.HorizontalSlider(SettingSliderRect(leftIndent, driverLines, contentWidth), + ActiveDriver.AvoidMass, 0, ActiveDriver.UpToEleven ? 1000000f : 100); + ActiveDriver.AvoidMass = Mathf.Round(ActiveDriver.AvoidMass); + } + else + { + inputFields["AvoidMass"].tryParseValue(GUI.TextField(SettingTextRect(leftIndent, driverLines, contentWidth), inputFields["AvoidMass"].possibleValue, 7, inputFieldStyle)); + ActiveDriver.AvoidMass = (float)inputFields["AvoidMass"].currentValue; + } + GUI.Label(SettinglabelRect(leftIndent, driverLines), StringUtils.Localize("#LOC_BDArmory_MinObstacleMass") + ": " + ActiveDriver.AvoidMass.ToString("0"), Label);//"Steer Damping" driverLines++; - } - - if (broadsideDir != (broadsideDir = Mathf.RoundToInt(GUI.HorizontalSlider(SettingSliderRect(leftIndent, driverLines, contentWidth), broadsideDir, 0, ActiveDriver.orbitDirections.Length - 1)))) - { - ActiveDriver.SetBroadsideDirection(ActiveDriver.orbitDirections[broadsideDir]); - ActiveDriver.ChooseOptionsUpdated(null, null); - } - GUI.Label(SettinglabelRect(leftIndent, driverLines), Localizer.Format("#LOC_BDArmory_PreferredBroadsideDirection") + ": " + ActiveDriver.OrbitDirectionName, Label);//"Wobbly" + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(leftIndent, driverLines), StringUtils.Localize("#LOC_BDArmory_DriverAI_Mass"), contextLabel);//"Wobbly" + driverLines++; + } - driverLines++; - if (contextTipsEnabled) - { - GUI.Label(ContextLabelRect(leftIndent, driverLines), Localizer.Format("#LOC_BDArmory_DriverAI_BroadsideDir"), contextLabel);//"Wobbly" + if (broadsideDir != (broadsideDir = Mathf.RoundToInt(GUI.HorizontalSlider(SettingSliderRect(leftIndent, driverLines, contentWidth), broadsideDir, 0, ActiveDriver.orbitDirections.Length - 1)))) + { + ActiveDriver.SetBroadsideDirection(ActiveDriver.orbitDirections[broadsideDir]); + ActiveDriver.ChooseOptionsUpdated(null, null); + } + GUI.Label(SettinglabelRect(leftIndent, driverLines), StringUtils.Localize("#LOC_BDArmory_PreferredBroadsideDirection") + ": " + ActiveDriver.OrbitDirectionName, Label);//"Wobbly" driverLines++; + if (contextTipsEnabled) + { + GUI.Label(ContextLabelRect(leftIndent, driverLines), StringUtils.Localize("#LOC_BDArmory_DriverAI_BroadsideDir"), contextLabel);//"Wobbly" + driverLines++; + } } - - GUI.EndGroup(); - height = Mathf.Max(Mathf.Lerp(height, driverLines * entryHeight + contentTop + 5, 1), (line * entryHeight) + contentTop); + contentHeight = driverLines * entryHeight; GUI.EndGroup(); GUI.EndScrollView(); } } - WindowHeight = contentTop + (line * entryHeight) + 5; WindowWidth = Mathf.Lerp(WindowWidth, windowColumns * ColumnWidth, 0.15f); + + #region Resizing + var resizeRect = new Rect(WindowWidth - 16, WindowHeight - 16, 16, 16); + GUI.DrawTexture(resizeRect, GUIUtils.resizeTexture, ScaleMode.StretchToFill, true); + if (Event.current.type == EventType.MouseDown && resizeRect.Contains(Event.current.mousePosition)) + { + resizingWindow = true; + } + + if (Event.current.type == EventType.Repaint && resizingWindow) + { + WindowHeight += Mouse.delta.y; + WindowHeight = Mathf.Max(WindowHeight, 305); + GUI.Label(new Rect(WindowWidth / 2, WindowHeight - 26, WindowWidth / 2 - 26, 26), $"Resizing: {WindowHeight}", Label); + } + #endregion + var previousWindowHeight = BDArmorySetup.WindowRectAI.height; BDArmorySetup.WindowRectAI.height = WindowHeight; BDArmorySetup.WindowRectAI.width = WindowWidth; - if (BDArmorySettings.STRICT_WINDOW_BOUNDARIES && WindowHeight < previousWindowHeight && Mathf.Round(BDArmorySetup.WindowRectAI.y + previousWindowHeight) == Screen.height) // Window shrunk while being at edge of screen. + if (!resizingWindow && BDArmorySettings.STRICT_WINDOW_BOUNDARIES && WindowHeight < previousWindowHeight && Mathf.Round(BDArmorySetup.WindowRectAI.y + previousWindowHeight) == Screen.height) // Window shrunk while being at edge of screen. BDArmorySetup.WindowRectAI.y = Screen.height - BDArmorySetup.WindowRectAI.height; - BDGUIUtils.RepositionWindow(ref BDArmorySetup.WindowRectAI); } #endregion GUI diff --git a/BDArmory/UI/BDArmorySetup.cs b/BDArmory/UI/BDArmorySetup.cs index f162064c4..05d6b7b10 100644 --- a/BDArmory/UI/BDArmorySetup.cs +++ b/BDArmory/UI/BDArmorySetup.cs @@ -1,3 +1,4 @@ +using System.Runtime.CompilerServices; using System; using System.IO; using System.Collections; @@ -5,22 +6,28 @@ using System.Globalization; using System.Linq; using System.Reflection; +using UnityEngine; +using KSP.Localization; +using KSP.UI.Screens; + +using BDArmory.Armor; using BDArmory.Bullets; -using BDArmory.Competition; using BDArmory.Competition.RemoteOrchestration; -using BDArmory.Competition.VesselSpawning; -using BDArmory.GameModes; -using BDArmory.Core; -using BDArmory.Core.Extension; +using BDArmory.Competition; +using BDArmory.Control; using BDArmory.CounterMeasure; +using BDArmory.Evolution; +using BDArmory.Extensions; using BDArmory.FX; -using BDArmory.Misc; +using BDArmory.GameModes; +using BDArmory.ModIntegration; using BDArmory.Modules; -using BDArmory.Parts; using BDArmory.Radar; -using UnityEngine; -using KSP.Localization; -using KSP.UI.Screens; +using BDArmory.Settings; +using BDArmory.Targeting; +using BDArmory.Utils; +using BDArmory.VesselSpawning; +using BDArmory.Weapons; namespace BDArmory.UI { @@ -43,6 +50,7 @@ public class BDArmorySetup : MonoBehaviour [BDAWindowSettingsField] public static Rect WindowRectRemoteOrchestration;// = new Rect(45, 100, 200, 200); [BDAWindowSettingsField] public static Rect WindowRectEvolution; [BDAWindowSettingsField] public static Rect WindowRectVesselSpawner; + [BDAWindowSettingsField] public static Rect WindowRectVesselMover; [BDAWindowSettingsField] public static Rect WindowRectAI; //reflection field lists @@ -82,10 +90,10 @@ static FieldInfo[] inputFields public static int numberOfParticleEmitters = 0; public static BDArmorySetup Instance; public static bool GAME_UI_ENABLED = true; - public string Version { get; private set; } = "Unknown"; + public static string Version { get; private set; } = "Unknown"; //toolbar button - static bool toolbarButtonAdded = false; + public static bool toolbarButtonAdded = false; //settings gui public static bool windowSettingsEnabled; @@ -94,8 +102,14 @@ static FieldInfo[] inputFields //editor alignment public static bool showWeaponAlignment; + //check for Apple Silicon + public static bool AppleSilicon = false; + // Gui Skin public static GUISkin BDGuiSkin = HighLogic.Skin; + public static GUIStyle ButtonStyle; + public static GUIStyle SelectedButtonStyle; + public static GUIStyle CloseButtonStyle; //toolbar gui public static bool hasAddedButton = false; @@ -169,6 +183,8 @@ public bool showingWindowGPS GUIStyle waterMarkStyle; GUIStyle redErrorStyle; GUIStyle redErrorShadowStyle; + GUIStyle inputFieldStyle; + bool stylesConfigured = false; public SortedList Teams = new SortedList { @@ -189,12 +205,14 @@ public static float SystemMaxMemory } } string CheatCodeGUI = ""; - string HoSString = ""; + string HoSString = ""; public string HoSTag = ""; bool enteredHoS = false; //competition mode - string compDistGui = "1000"; + string compDistGui; + string compIntraTeamSeparationBase; + string compIntraTeamSeparationPerMember; #region Textures @@ -243,6 +261,19 @@ public Texture2D greenDotTexture get { return gdott ? gdott : gdott = GameDatabase.Instance.GetTexture(textureDir + "greenDot", false); } } + private Texture2D rdott; + + public Texture2D redDotTexture + { + get { return rdott ? rdott : rdott = GameDatabase.Instance.GetTexture(textureDir + "redDot", false); } + } + + private Texture2D rspike; + + public Texture2D irSpikeTexture + { + get { return rspike ? rspike : rspike = GameDatabase.Instance.GetTexture(textureDir + "IRspike", false); } + } private Texture2D gdt; public Texture2D greenDiamondTexture @@ -347,7 +378,7 @@ public Texture2D FiringAngleImage public static bool GameIsPaused { - get { return PauseMenu.isOpen || Time.timeScale == 0; } + get { return HighLogic.LoadedSceneIsFlight && (PauseMenu.isOpen || Time.timeScale == 0); } } void Awake() @@ -386,9 +417,17 @@ void Awake() WindowRectGps.width = WindowRectToolbar.width - 10; + // Get the BDA version. We can do this here since it's this assembly we're interested in, other assemblies have to wait until Start. + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies().ToList()) + if (assembly.FullName.Split(new char[1] { ',' })[0] == "BDArmory") + Version = assembly.GetName().Version.ToString(); + // Load settings LoadConfig(); + // Check for Apple Processor + AppleSilicon = CultureInfo.InvariantCulture.CompareInfo.IndexOf(SystemInfo.processorType, "Apple", CompareOptions.IgnoreCase) >= 0; + // Ensure AutoSpawn folder exists. if (!Directory.Exists(Path.Combine(KSPUtil.ApplicationRootPath, "AutoSpawn"))) { Directory.CreateDirectory(Path.Combine(KSPUtil.ApplicationRootPath, "AutoSpawn")); } @@ -401,8 +440,11 @@ void Start() { //wmgr toolbar if (HighLogic.LoadedSceneIsFlight) + { saveWindowPosition = true; //otherwise later we should NOT save the current window positions! - + CheatOptions.InfinitePropellant = BDArmorySettings.INFINITE_FUEL; + CheatOptions.InfiniteElectricity = BDArmorySettings.INFINITE_EC; + } // // Create settings file if not present. // if (ConfigNode.Load(BDArmorySettings.settingsConfigURL) == null) // { @@ -429,6 +471,86 @@ void Start() fireKeyGui = BDInputSettingsFields.WEAP_FIRE_KEY.inputString; //setup gui styles + CloseButtonStyle = new GUIStyle(BDGuiSkin.button) { alignment = TextAnchor.MiddleCenter }; // Configure this one separately since it's static. + CloseButtonStyle.hover.textColor = Color.red; + + ButtonStyle = new GUIStyle(BDArmorySetup.BDGuiSkin.button); + SelectedButtonStyle = new GUIStyle(BDArmorySetup.BDGuiSkin.button); + var tmp = SelectedButtonStyle.normal; + SelectedButtonStyle.normal = SelectedButtonStyle.active; + SelectedButtonStyle.active = tmp; + SelectedButtonStyle.hover = SelectedButtonStyle.normal; + // + + using (var a = AppDomain.CurrentDomain.GetAssemblies().ToList().GetEnumerator()) + while (a.MoveNext()) + { + string name = a.Current.FullName.Split(new char[1] { ',' })[0]; + switch (name) + { + case "ModuleManager": + ModuleManagerLoaded = true; + break; + + case "PhysicsRangeExtender": + foreach (var t in a.Current.GetTypes()) + { + if (t != null && t.Name == "PreSettings") + { + var PREInstance = FindObjectOfType(t); + foreach (var propInfo in t.GetProperties(BindingFlags.Public | BindingFlags.Static)) + if (propInfo != null && propInfo.Name == "ModEnabled") + { + PREModEnabledField = propInfo; + PhysicsRangeExtenderLoaded = true; + } + } + } + break; + } + } + + if (HighLogic.LoadedSceneIsFlight) + { + SaveVolumeSettings(); + + GameEvents.onHideUI.Add(HideGameUI); + GameEvents.onShowUI.Add(ShowGameUI); + GameEvents.onVesselGoOffRails.Add(OnVesselGoOffRails); + GameEvents.OnGameSettingsApplied.Add(SaveVolumeSettings); + + GameEvents.onVesselChange.Add(VesselChange); + } + + BulletInfo.Load(); + RocketInfo.Load(); + ArmorInfo.Load(); + MutatorInfo.Load(); + HullInfo.Load(); + ProjectileUtils.SetUpPartsHashSets(); + ProjectileUtils.SetUpWeaponReporting(); + + compDistGui = BDArmorySettings.COMPETITION_DISTANCE.ToString(); + compIntraTeamSeparationBase = BDArmorySettings.COMPETITION_INTRA_TEAM_SEPARATION_BASE.ToString(); + compIntraTeamSeparationPerMember = BDArmorySettings.COMPETITION_INTRA_TEAM_SEPARATION_PER_MEMBER.ToString(); + HoSTag = BDArmorySettings.HOS_BADGE; + + if (HighLogic.LoadedSceneIsFlight || HighLogic.LoadedSceneIsEditor) + { StartCoroutine(ToolbarButtonRoutine()); } + + for (int i = 0; i < MutatorInfo.mutators.Count; i++) + { + mutators.Add(MutatorInfo.mutators[i].name); + } + mutators_selected = new bool[mutators.Count]; + for (int i = 0; i < mutators_selected.Length; ++i) + { + mutators_selected[i] = BDArmorySettings.MUTATOR_LIST.Contains(mutators[i]); + } + } + + void ConfigureStyles() + { centerLabel = new GUIStyle(); centerLabel.alignment = TextAnchor.UpperCenter; centerLabel.normal.textColor = Color.white; @@ -506,81 +628,39 @@ void Start() redErrorShadowStyle = new GUIStyle(redErrorStyle); redErrorShadowStyle.normal.textColor = new Color(0, 0, 0, 0.75f); - // - - using (var a = AppDomain.CurrentDomain.GetAssemblies().ToList().GetEnumerator()) - while (a.MoveNext()) - { - string name = a.Current.FullName.Split(new char[1] { ',' })[0]; - switch (name) - { - case "ModuleManager": - ModuleManagerLoaded = true; - break; - case "PhysicsRangeExtender": - foreach (var t in a.Current.GetTypes()) - { - if (t != null && t.Name == "PreSettings") - { - var PREInstance = FindObjectOfType(t); - foreach (var propInfo in t.GetProperties(BindingFlags.Public | BindingFlags.Static)) - if (propInfo != null && propInfo.Name == "ModEnabled") - { - PREModEnabledField = propInfo; - PhysicsRangeExtenderLoaded = true; - } - } - } - break; + inputFieldStyle = new GUIStyle(GUI.skin.textField); + inputFieldStyle.alignment = TextAnchor.UpperRight; - case "BDArmory": - Version = a.Current.GetName().Version.ToString(); - break; - } - } - - if (HighLogic.LoadedSceneIsFlight) - { - SaveVolumeSettings(); - - GameEvents.onHideUI.Add(HideGameUI); - GameEvents.onShowUI.Add(ShowGameUI); - GameEvents.onVesselGoOffRails.Add(OnVesselGoOffRails); - GameEvents.OnGameSettingsApplied.Add(SaveVolumeSettings); - - GameEvents.onVesselChange.Add(VesselChange); - } - - BulletInfo.Load(); - RocketInfo.Load(); - ArmorInfo.Load(); - MutatorInfo.Load(); - - compDistGui = BDArmorySettings.COMPETITION_DISTANCE.ToString(); - HoSTag = BDArmorySettings.HOS_BADGE; - - if (HighLogic.LoadedSceneIsFlight || HighLogic.LoadedSceneIsEditor) - { StartCoroutine(ToolbarButtonRoutine()); } + stylesConfigured = true; + } - for (int i = 0; i < MutatorInfo.mutators.Count; i++) - { - mutators.Add(MutatorInfo.mutators[i].name); - } - mutators_selected = new bool[mutators.Count]; - for (int i = 0; i < mutators_selected.Length; ++i) + /// + /// Modify the background opacity of a window. + /// + /// GUI.Window stores the color values it was called with, so call this with enable=true before GUI.Window to enable + /// transparency for that window and again with enable=false afterwards to avoid affect later GUI.Window calls. + /// + /// Note: This can only lower the opacity of the window background, so windows with a background texture that + /// already includes some transparency can only be made more transparent, not less. + /// + /// Enable or reset the modified background opacity. + public static void SetGUIOpacity(bool enable = true) + { + if (!enable && BDArmorySettings.GUI_OPACITY == 1f) return; // Nothing to do. + var guiColor = GUI.backgroundColor; + if (guiColor.a != (enable ? BDArmorySettings.GUI_OPACITY : 1f)) { - mutators_selected[i] = BDArmorySettings.MUTATOR_LIST.Contains(mutators[i]); + guiColor.a = (enable ? BDArmorySettings.GUI_OPACITY : 1f); + GUI.backgroundColor = guiColor; } } IEnumerator ToolbarButtonRoutine() { if (toolbarButtonAdded) yield break; - while (!ApplicationLauncher.Ready) - { yield return null; } + yield return new WaitUntil(() => ApplicationLauncher.Ready); if (toolbarButtonAdded) yield break; - toolbarButtonAdded = true; Texture buttonTexture = GameDatabase.Instance.GetTexture(BDArmorySetup.textureDir + "icon", false); ApplicationLauncher.Instance.AddModApplication( ToggleToolbarButton, @@ -592,6 +672,7 @@ IEnumerator ToolbarButtonRoutine() ApplicationLauncher.AppScenes.FLIGHT | ApplicationLauncher.AppScenes.SPH | ApplicationLauncher.AppScenes.VAB, buttonTexture ); + toolbarButtonAdded = true; } /// /// Toggle the BDAToolbar or BDA settings window depending on the scene. @@ -604,15 +685,15 @@ void ToggleToolbarButton() private void CheckIfWindowsSettingsAreWithinScreen() { - BDGUIUtils.RepositionWindow(ref WindowRectEvolution); - BDGUIUtils.UseMouseEventInRect(WindowRectSettings); - BDGUIUtils.RepositionWindow(ref WindowRectToolbar); - BDGUIUtils.RepositionWindow(ref WindowRectSettings); - BDGUIUtils.RepositionWindow(ref WindowRectRwr); - BDGUIUtils.RepositionWindow(ref WindowRectVesselSwitcher); - BDGUIUtils.RepositionWindow(ref WindowRectWingCommander); - BDGUIUtils.RepositionWindow(ref WindowRectTargetingCam); - BDGUIUtils.RepositionWindow(ref WindowRectAI); + GUIUtils.RepositionWindow(ref WindowRectEvolution); + GUIUtils.UseMouseEventInRect(WindowRectSettings); + GUIUtils.RepositionWindow(ref WindowRectToolbar); + GUIUtils.RepositionWindow(ref WindowRectSettings); + GUIUtils.RepositionWindow(ref WindowRectRwr); + GUIUtils.RepositionWindow(ref WindowRectVesselSwitcher); + GUIUtils.RepositionWindow(ref WindowRectWingCommander); + GUIUtils.RepositionWindow(ref WindowRectTargetingCam); + GUIUtils.RepositionWindow(ref WindowRectAI); } void Update() @@ -631,8 +712,7 @@ void Update() if (BDInputUtils.GetKeyDown(BDInputSettingsFields.TIME_SCALING)) { - BDArmorySettings.TIME_OVERRIDE = !BDArmorySettings.TIME_OVERRIDE; - Time.timeScale = BDArmorySettings.TIME_OVERRIDE ? BDArmorySettings.TIME_SCALE : 1f; + OtherUtils.SetTimeOverride(!BDArmorySettings.TIME_OVERRIDE); } } else if (HighLogic.LoadedSceneIsEditor) @@ -670,14 +750,6 @@ void ToggleWindowSettings() } } - void LateUpdate() - { - if (HighLogic.LoadedSceneIsFlight) - { - //UpdateCursorState(); - } - } - public void UpdateCursorState() { if (ActiveWeaponManager == null) @@ -698,7 +770,7 @@ public void UpdateCursorState() if (HighLogic.LoadedSceneIsFlight) { drawCursor = false; - if (!MapView.MapIsEnabled && !Utils.CheckMouseIsOnGui() && !PauseMenu.isOpen) + if (!MapView.MapIsEnabled && !GUIUtils.CheckMouseIsOnGui() && !PauseMenu.isOpen) { if (ActiveWeaponManager.selectedWeapon != null && ActiveWeaponManager.weaponIndex > 0 && !ActiveWeaponManager.guardMode) @@ -708,7 +780,7 @@ public void UpdateCursorState() ActiveWeaponManager.selectedWeapon.GetWeaponClass() == WeaponClasses.DefenseLaser) { ModuleWeapon mw = ActiveWeaponManager.selectedWeapon.GetPart().FindModuleImplementing(); - if (mw != null && mw.weaponState == ModuleWeapon.WeaponStates.Enabled && mw.maxPitch > 1 && !mw.slaved && !mw.aiControlled) + if (mw != null && mw.weaponState == ModuleWeapon.WeaponStates.Enabled && mw.maxPitch > 1 && !mw.slaved && !mw.GPSTarget && !mw.aiControlled) { //Screen.showCursor = false; Cursor.visible = false; @@ -717,6 +789,12 @@ public void UpdateCursorState() } } } + + if (MouseAimFlight.IsMouseAimActive) + { + Cursor.visible = false; + return; + } } } @@ -750,16 +828,19 @@ public void ConfigTextFields() { "guardRange", gameObject.AddComponent().Initialise(0, ActiveWeaponManager.guardRange, 100, BDArmorySettings.MAX_GUARD_VISUAL_RANGE) }, { "gunRange", gameObject.AddComponent().Initialise(0, ActiveWeaponManager.gunRange, 0, ActiveWeaponManager.maxGunRange) }, { "multiTargetNum", gameObject.AddComponent().Initialise(0, ActiveWeaponManager.multiTargetNum, 1, 10) }, + { "multiMissileTgtNum", gameObject.AddComponent().Initialise(0, ActiveWeaponManager.multiMissileTgtNum, 1, 10) }, { "maxMissilesOnTarget", gameObject.AddComponent().Initialise(0, ActiveWeaponManager.maxMissilesOnTarget, 1, MissileFire.maxAllowableMissilesOnTarget) }, { "targetBias", gameObject.AddComponent().Initialise(0, ActiveWeaponManager.targetBias, -10, 10) }, { "targetWeightRange", gameObject.AddComponent().Initialise(0, ActiveWeaponManager.targetWeightRange, -10, 10) }, + { "targetWeightAirPreference", gameObject.AddComponent().Initialise(0, ActiveWeaponManager.targetWeightAirPreference, -10, 10) }, { "targetWeightATA", gameObject.AddComponent().Initialise(0, ActiveWeaponManager.targetWeightATA, -10, 10) }, { "targetWeightAoD", gameObject.AddComponent().Initialise(0, ActiveWeaponManager.targetWeightAoD, -10, 10) }, { "targetWeightAccel", gameObject.AddComponent().Initialise(0, ActiveWeaponManager.targetWeightAccel,-10, 10) }, { "targetWeightClosureTime", gameObject.AddComponent().Initialise(0, ActiveWeaponManager.targetWeightClosureTime, -10, 10) }, { "targetWeightWeaponNumber", gameObject.AddComponent().Initialise(0, ActiveWeaponManager.targetWeightWeaponNumber, -10, 10) }, { "targetWeightMass", gameObject.AddComponent().Initialise(0, ActiveWeaponManager.targetWeightMass,-10, 10) }, + { "targetWeightDamage", gameObject.AddComponent().Initialise(0, ActiveWeaponManager.targetWeightDamage,-10, 10) }, { "targetWeightFriendliesEngaging", gameObject.AddComponent().Initialise(0, ActiveWeaponManager.targetWeightFriendliesEngaging, -10, 10) }, { "targetWeightThreat", gameObject.AddComponent().Initialise(0, ActiveWeaponManager.targetWeightThreat, -10, 10) }, { "targetWeightProtectTeammate", gameObject.AddComponent().Initialise(0, ActiveWeaponManager.targetWeightProtectTeammate, -10, 10) }, @@ -768,19 +849,25 @@ public void ConfigTextFields() }; } + static bool firstLoad = true; public static void LoadConfig() { try { Debug.Log("[BDArmory.BDArmorySetup]=== Loading settings.cfg ==="); + if (firstLoad) + { + BDAPersistentSettingsField.Upgrade(); + firstLoad = false; + } BDAPersistentSettingsField.Load(); BDInputSettingsFields.LoadSettings(); BDArmorySettings.ready = true; } catch (NullReferenceException e) { - Debug.LogWarning("[BDArmory.BDArmorySetup]=== Failed to load settings config ===: " + e.Message); + Debug.LogError("[BDArmory.BDArmorySetup]=== Failed to load settings config ===: " + e.Message + "\n" + e.StackTrace); } } @@ -790,7 +877,7 @@ public static void SaveConfig() { Debug.Log("[BDArmory.BDArmorySetup] == Saving settings.cfg == "); - BDAPersistentSettingsField.Save(); + BDAPersistentSettingsField.Save(BDArmorySettings.settingsConfigURL); BDInputSettingsFields.SaveSettings(); @@ -801,7 +888,7 @@ public static void SaveConfig() } catch (NullReferenceException e) { - Debug.LogWarning("[BDArmory.BDArmorySetup]: === Failed to save settings.cfg ====: " + e.Message); + Debug.LogError("[BDArmory.BDArmorySetup]: === Failed to save settings.cfg ====: " + e.Message + "\n" + e.StackTrace); } } @@ -810,6 +897,7 @@ public static void SaveConfig() void OnGUI() { if (!GAME_UI_ENABLED) return; + if (!stylesConfigured) ConfigureStyles(); if (windowSettingsEnabled) { WindowRectSettings = GUI.Window(129419, WindowRectSettings, WindowSettings, GUIContent.none); @@ -828,27 +916,24 @@ void OnGUI() } if (!windowBDAToolBarEnabled || !HighLogic.LoadedSceneIsFlight) return; + SetGUIOpacity(); WindowRectToolbar = GUI.Window(321, WindowRectToolbar, WindowBDAToolbar, "", BDGuiSkin.window);//"BDA Weapon Manager" - BDGUIUtils.UseMouseEventInRect(WindowRectToolbar); + SetGUIOpacity(false); + GUIUtils.UseMouseEventInRect(WindowRectToolbar); if (showWindowGPS && ActiveWeaponManager) { //gpsWindowRect = GUI.Window(424333, gpsWindowRect, GPSWindow, "", GUI.skin.box); - BDGUIUtils.UseMouseEventInRect(WindowRectGps); + GUIUtils.UseMouseEventInRect(WindowRectGps); using (var coord = BDATargetManager.GPSTargetList(ActiveWeaponManager.Team).GetEnumerator()) while (coord.MoveNext()) { - BDGUIUtils.DrawTextureOnWorldPos(coord.Current.worldPos, Instance.greenDotTexture, new Vector2(8, 8), 0); + GUIUtils.DrawTextureOnWorldPos(coord.Current.worldPos, Instance.greenDotTexture, new Vector2(8, 8), 0); } } if (Time.time - dependencyLastCheckTime > (dependencyWarnings.Count() == 0 ? 60 : 5)) // Only check once per minute if no issues are found, otherwise 5s. { - dependencyLastCheckTime = Time.time; - dependencyWarnings.Clear(); - if (!ModuleManagerLoaded) dependencyWarnings.Add("Module Manager dependency is missing!"); - if (!PhysicsRangeExtenderLoaded) dependencyWarnings.Add("Physics Range Extender dependency is missing!"); - else if (BDACompetitionMode.Instance != null && (BDACompetitionMode.Instance.competitionIsActive || BDACompetitionMode.Instance.competitionStarting) && !(bool)PREModEnabledField.GetValue(null)) dependencyWarnings.Add("Physics Range Extender is disabled!"); - if (dependencyWarnings.Count() > 0) dependencyWarnings.Add("BDArmory will not work properly."); + CheckDependencies(); } if (dependencyWarnings.Count() > 0) { @@ -857,11 +942,32 @@ void OnGUI() } } + /// + /// Check that the dependencies are satisfied. + /// + /// true if they are, false otherwise. + public bool CheckDependencies() + { + dependencyLastCheckTime = Time.time; + dependencyWarnings.Clear(); + if (!ModuleManagerLoaded) dependencyWarnings.Add("Module Manager dependency is missing!"); + if (!PhysicsRangeExtenderLoaded) dependencyWarnings.Add("Physics Range Extender dependency is missing!"); + else if (( + (BDACompetitionMode.Instance != null && (BDACompetitionMode.Instance.competitionIsActive || BDACompetitionMode.Instance.competitionStarting)) + || VesselSpawnerStatus.vesselsSpawning + ) + && !(bool)PREModEnabledField.GetValue(null)) dependencyWarnings.Add("Physics Range Extender is disabled!"); + if (dependencyWarnings.Count() > 0) dependencyWarnings.Add("BDArmory will not work properly."); + return dependencyWarnings.Count() == 0; + } + public bool hasVesselSwitcher = false; public bool hasVesselSpawner = false; + public bool hasVesselMover = false; public bool hasEvolution = false; - public bool showVesselSwitcherGUI = false; - public bool showVesselSpawnerGUI = false; + public static bool showVesselSwitcherGUI = false; + public static bool showVesselSpawnerGUI = false; + public static bool showVesselMoverGUI = false; public bool showEvolutionGUI = false; float rippleHeight; @@ -872,18 +978,28 @@ void OnGUI() float EngageHeight; float modulesHeight; float gpsHeight; - bool toolMinimized; + bool toolMinimized = false; + + float leftIndent = 10; + float guardLabelWidth = 90; + float priorityLabelWidth = 120; + float rightLabelWidth = 45; + float contentTop = 10; + float entryHeight = 20; + float _buttonSize = 26; + float _windowMargin = 4; + + Rect LabelRect(float line, float labelWidth) => new Rect(leftIndent + 3, line * entryHeight, labelWidth, entryHeight); + Rect SliderRect(float line, float labelWidth) => new Rect(leftIndent + labelWidth + 16, (line + 0.2f) * entryHeight, columnWidth - 2 * leftIndent - labelWidth - rightLabelWidth - 28, entryHeight); + Rect InputFieldRect(float line, float labelWidth) => new Rect(leftIndent + labelWidth + 16, line * entryHeight, columnWidth - 2 * leftIndent - labelWidth - 28, entryHeight); + Rect RightLabelRect(float line) => new Rect(columnWidth - leftIndent - 3 - rightLabelWidth, line * entryHeight, rightLabelWidth, entryHeight); + Rect ButtonRect(float line) => new Rect(leftIndent + 3, line * entryHeight, columnWidth - 2 * leftIndent - 16, entryHeight); void WindowBDAToolbar(int windowID) { float line = 0; - float leftIndent = 10; - float contentWidth = (columnWidth) - (2 * leftIndent); + float contentWidth = columnWidth - 2 * leftIndent; float windowColumns = 1; - float contentTop = 10; - float entryHeight = 20; - float _buttonSize = 26; - float _windowMargin = 4; int buttonNumber = 0; GUI.DragWindow(new Rect(_windowMargin + _buttonSize, 0, columnWidth - 2 * _windowMargin - numberOfButtons * _buttonSize, _windowMargin + _buttonSize)); @@ -892,7 +1008,7 @@ void WindowBDAToolbar(int windowID) line += 0.25f; //title - GUI.Label(new Rect(_windowMargin + _buttonSize, _windowMargin, columnWidth - 2 * _windowMargin - numberOfButtons * _buttonSize, _windowMargin + _buttonSize), Localizer.Format("#LOC_BDArmory_WMWindow_title") + " ", kspTitleLabel); + GUI.Label(new Rect(_windowMargin + _buttonSize, _windowMargin, columnWidth - 2 * _windowMargin - numberOfButtons * _buttonSize, _windowMargin + _buttonSize), StringUtils.Localize("#LOC_BDArmory_WMWindow_title") + " ", kspTitleLabel); // Version. GUI.Label(new Rect(columnWidth - _windowMargin - (numberOfButtons - 1) * _buttonSize - 100, 23, 57, 10), Version, waterMarkStyle); @@ -910,7 +1026,7 @@ void WindowBDAToolbar(int windowID) GUIStyle vsStyle = showVesselSwitcherGUI ? BDGuiSkin.box : BDGuiSkin.button; if (GUI.Button(new Rect(columnWidth - _windowMargin - ++buttonNumber * _buttonSize, _windowMargin, _buttonSize, _buttonSize), "VS", vsStyle)) { - showVesselSwitcherGUI = !showVesselSwitcherGUI; + LoadedVesselSwitcher.Instance.SetVisible(!showVesselSwitcherGUI); } } @@ -920,19 +1036,25 @@ void WindowBDAToolbar(int windowID) GUIStyle vsStyle = showVesselSpawnerGUI ? BDGuiSkin.box : BDGuiSkin.button; if (GUI.Button(new Rect(columnWidth - _windowMargin - ++buttonNumber * _buttonSize, _windowMargin, _buttonSize, _buttonSize), "Sp", vsStyle)) { - showVesselSpawnerGUI = !showVesselSpawnerGUI; + VesselSpawnerWindow.Instance.SetVisible(!showVesselSpawnerGUI); if (!showVesselSpawnerGUI) SaveConfig(); } } + // VesselMover button + if (hasVesselMover && GUI.Button(new Rect(columnWidth - _windowMargin - ++buttonNumber * _buttonSize, _windowMargin, _buttonSize, _buttonSize), "VM", showVesselMoverGUI ? BDGuiSkin.box : BDGuiSkin.button)) + { + VesselMover.Instance.SetVisible(!showVesselMoverGUI); + } + // evolution button if (BDArmorySettings.EVOLUTION_ENABLED && hasEvolution) { var evolutionSkin = showEvolutionGUI ? BDGuiSkin.box : BDGuiSkin.button; ; if (GUI.Button(new Rect(columnWidth - _windowMargin - ++buttonNumber * _buttonSize, _windowMargin, _buttonSize, _buttonSize), "EV", evolutionSkin)) { - showEvolutionGUI = !showEvolutionGUI; + EvolutionWindow.Instance.SetVisible(!showEvolutionGUI); } } @@ -1001,29 +1123,28 @@ void WindowBDAToolbar(int windowID) if (ActiveWeaponManager != null) { //MINIMIZE BUTTON - toolMinimized = GUI.Toggle(new Rect(_windowMargin, _windowMargin, _buttonSize, _buttonSize), toolMinimized, "_", - toolMinimized ? BDGuiSkin.box : BDGuiSkin.button); + toolMinimized = GUI.Toggle(new Rect(_windowMargin, _windowMargin, _buttonSize, _buttonSize), toolMinimized, "_", toolMinimized ? BDGuiSkin.box : BDGuiSkin.button); GUIStyle armedLabelStyle; Rect armedRect = new Rect(leftIndent, contentTop + (line * entryHeight), contentWidth / 2, entryHeight); if (ActiveWeaponManager.guardMode) { - if (GUI.Button(armedRect, "- " + Localizer.Format("#LOC_BDArmory_WMWindow_GuardModebtn") + " -", BDGuiSkin.box))//Guard Mode + if (GUI.Button(armedRect, "- " + StringUtils.Localize("#LOC_BDArmory_WMWindow_GuardModebtn") + " -", BDGuiSkin.box))//Guard Mode { showGuardMenu = true; } } else { - string armedText = Localizer.Format("#LOC_BDArmory_WMWindow_ArmedText");//"Trigger is " + string armedText = StringUtils.Localize("#LOC_BDArmory_WMWindow_ArmedText");//"Trigger is " if (ActiveWeaponManager.isArmed) { - armedText += Localizer.Format("#LOC_BDArmory_WMWindow_ArmedText_ARMED");//"ARMED." + armedText += StringUtils.Localize("#LOC_BDArmory_WMWindow_ArmedText_ARMED");//"ARMED." armedLabelStyle = BDGuiSkin.box; } else { - armedText += Localizer.Format("#LOC_BDArmory_WMWindow_ArmedText_DisArmed");//"disarmed." + armedText += StringUtils.Localize("#LOC_BDArmory_WMWindow_ArmedText_DisArmed");//"disarmed." armedLabelStyle = BDGuiSkin.button; } if (GUI.Button(armedRect, armedText, armedLabelStyle)) @@ -1033,7 +1154,7 @@ void WindowBDAToolbar(int windowID) } GUIStyle teamButtonStyle = BDGuiSkin.box; - string teamText = Localizer.Format("#LOC_BDArmory_WMWindow_TeamText") + ": " + ActiveWeaponManager.Team.Name + (ActiveWeaponManager.Team.Neutral ? (ActiveWeaponManager.Team.Name != "Neutral" ? "(N)" : "") : "");//Team + string teamText = StringUtils.Localize("#LOC_BDArmory_WMWindow_TeamText") + $": {ActiveWeaponManager.Team.Name + (ActiveWeaponManager.Team.Neutral ? (ActiveWeaponManager.Team.Name != "Neutral" ? "(N)" : "") : "")}";//Team if (GUI.Button(new Rect(leftIndent + (contentWidth / 2), contentTop + (line * entryHeight), contentWidth / 2, entryHeight), teamText, teamButtonStyle)) { if (Event.current.button == 1) @@ -1048,8 +1169,7 @@ void WindowBDAToolbar(int windowID) line++; line += 0.25f; string weaponName = ActiveWeaponManager.selectedWeaponString; - // = ActiveWeaponManager.selectedWeapon == null ? "None" : ActiveWeaponManager.selectedWeapon.GetShortName(); - string selectionText = Localizer.Format("#LOC_BDArmory_WMWindow_selectionText", weaponName);//Weapon: <<1>> + string selectionText = StringUtils.Localize("#LOC_BDArmory_WMWindow_selectionText", weaponName);//Weapon: <<1>> GUI.Label(new Rect(leftIndent, contentTop + (line * entryHeight), contentWidth, entryHeight * 1.25f), selectionText, BDGuiSkin.box); line += 1.25f; line += 0.1f; @@ -1062,8 +1182,8 @@ void WindowBDAToolbar(int windowID) || ActiveWeaponManager.selectedWeapon.GetWeaponClass() == WeaponClasses.DefenseLaser)) //remove rocket ripple slider - moved to editor { string rippleText = ActiveWeaponManager.rippleFire - ? Localizer.Format("#LOC_BDArmory_WMWindow_rippleText1", ActiveWeaponManager.gunRippleRpm.ToString("0"))//"Barrage: " + + " RPM" - : Localizer.Format("#LOC_BDArmory_WMWindow_rippleText2");//"Salvo" + ? StringUtils.Localize("#LOC_BDArmory_WMWindow_rippleText1", ActiveWeaponManager.gunRippleRpm.ToString("0"))//"Barrage: " + + " RPM" + : StringUtils.Localize("#LOC_BDArmory_WMWindow_rippleText2");//"Salvo" GUIStyle rippleStyle = ActiveWeaponManager.rippleFire ? BDGuiSkin.box : BDGuiSkin.button; @@ -1074,14 +1194,19 @@ void WindowBDAToolbar(int windowID) { ActiveWeaponManager.ToggleRippleFire(); } + if (ActiveWeaponManager.rippleFire) + { + GUI.Label(new Rect(leftIndent + contentWidth / 2 + _windowMargin, contentTop + line * entryHeight, contentWidth / 4 - _windowMargin, entryHeight * 1.25f), $"{StringUtils.Localize("#LOC_BDArmory_WMWindow_barrageStagger")}: {(ActiveWeaponManager.barrageStagger > 0 ? ActiveWeaponManager.barrageStagger : 1):G1}"); + ActiveWeaponManager.barrageStagger = BDAMath.RoundToUnit(GUI.HorizontalSlider(new Rect(leftIndent + 3 * contentWidth / 4, contentTop + (line + 0.25f) * entryHeight, contentWidth / 4, entryHeight), ActiveWeaponManager.barrageStagger, 0f, 0.1f), 0.01f); + } rippleHeight = Mathf.Lerp(rippleHeight, 1.25f, 0.15f); } else { string rippleText = ActiveWeaponManager.rippleFire - ? Localizer.Format("#LOC_BDArmory_WMWindow_rippleText3", ActiveWeaponManager.rippleRPM.ToString("0"))//"Ripple: " + + " RPM" - : Localizer.Format("#LOC_BDArmory_WMWindow_rippleText4");//"Ripple: OFF" + ? StringUtils.Localize("#LOC_BDArmory_WMWindow_rippleText3", ActiveWeaponManager.rippleRPM.ToString("0"))//"Ripple: " + + " RPM" + : StringUtils.Localize("#LOC_BDArmory_WMWindow_rippleText4");//"Ripple: OFF" GUIStyle rippleStyle = ActiveWeaponManager.rippleFire ? BDGuiSkin.box : BDGuiSkin.button; @@ -1094,17 +1219,15 @@ void WindowBDAToolbar(int windowID) } if (ActiveWeaponManager.rippleFire) { - Rect sliderRect = new Rect(leftIndent + (contentWidth / 2) + 2, - contentTop + (line * entryHeight) + 6.5f, (contentWidth / 2) - 2, 12); - if (!NumFieldsEnabled) { - ActiveWeaponManager.rippleRPM = GUI.HorizontalSlider(sliderRect, + ActiveWeaponManager.rippleRPM = GUI.HorizontalSlider(new Rect(leftIndent + (contentWidth / 2) + 2, contentTop + (line * entryHeight) + 6.5f, (contentWidth / 2) - 2, 12), ActiveWeaponManager.rippleRPM, 100, 1600, rippleSliderStyle, rippleThumbStyle); } else { - textNumFields["rippleRPM"].tryParseValue(GUI.TextField(sliderRect, textNumFields["rippleRPM"].possibleValue, 4)); + textNumFields["rippleRPM"].tryParseValue(GUI.TextField(new Rect(leftIndent + (contentWidth / 2) + 2, contentTop + (line * entryHeight) + 6.5f, (contentWidth / 2) - 2, entryHeight), + textNumFields["rippleRPM"].possibleValue, 4, inputFieldStyle)); ActiveWeaponManager.rippleRPM = (float)textNumFields["rippleRPM"].currentValue; } } @@ -1115,7 +1238,6 @@ void WindowBDAToolbar(int windowID) { rippleHeight = Mathf.Lerp(rippleHeight, 0, 0.15f); } - //line += 1.25f; line += rippleHeight; line += 0.1f; @@ -1123,20 +1245,20 @@ void WindowBDAToolbar(int windowID) { showWeaponList = GUI.Toggle(new Rect(leftIndent, contentTop + (line * entryHeight), contentWidth / 4, entryHeight), - showWeaponList, Localizer.Format("#LOC_BDArmory_WMWindow_ListWeapons"), showWeaponList ? BDGuiSkin.box : BDGuiSkin.button);//"Weapons" + showWeaponList, StringUtils.Localize("#LOC_BDArmory_WMWindow_ListWeapons"), showWeaponList ? BDGuiSkin.box : BDGuiSkin.button);//"Weapons" showGuardMenu = GUI.Toggle( new Rect(leftIndent + (contentWidth / 4), contentTop + (line * entryHeight), contentWidth / 4, - entryHeight), showGuardMenu, Localizer.Format("#LOC_BDArmory_WMWindow_GuardMenu"),//"Guard Menu" + entryHeight), showGuardMenu, StringUtils.Localize("#LOC_BDArmory_WMWindow_GuardMenu"),//"Guard Menu" showGuardMenu ? BDGuiSkin.box : BDGuiSkin.button); showPriorities = GUI.Toggle(new Rect(leftIndent + (2 * contentWidth / 4), contentTop + (line * entryHeight), contentWidth / 4, - entryHeight), showPriorities, Localizer.Format("#LOC_BDArmory_WMWindow_TargetPriority"),//"Tgt priority" + entryHeight), showPriorities, StringUtils.Localize("#LOC_BDArmory_WMWindow_TargetPriority"),//"Tgt priority" showPriorities ? BDGuiSkin.box : BDGuiSkin.button); showModules = GUI.Toggle( new Rect(leftIndent + (3 * contentWidth / 4), contentTop + (line * entryHeight), contentWidth / 4, - entryHeight), showModules, Localizer.Format("#LOC_BDArmory_WMWindow_ModulesToggle"),//"Modules" + entryHeight), showModules, StringUtils.Localize("#LOC_BDArmory_WMWindow_ModulesToggle"),//"Modules" showModules ? BDGuiSkin.box : BDGuiSkin.button); line++; } @@ -1172,11 +1294,10 @@ void WindowBDAToolbar(int windowID) } else { - label = Localizer.Format("#LOC_BDArmory_WMWindow_NoneWeapon");//"None" + label = StringUtils.Localize("#LOC_BDArmory_WMWindow_NoneWeapon");//"None" subLabel = String.Empty; } - Rect weaponButtonRect = new Rect(leftIndent, (weaponLines * entryHeight), - weaponListGroupRect.width - (2 * leftIndent), entryHeight); + Rect weaponButtonRect = new Rect(leftIndent, (weaponLines * entryHeight), weaponListGroupRect.width - (2 * leftIndent), entryHeight); GUI.Label(weaponButtonRect, subLabel, tgtStyle); @@ -1187,7 +1308,7 @@ void WindowBDAToolbar(int windowID) if (i < ActiveWeaponManager.weaponArray.Length - 1) { - BDGUIUtils.DrawRectangle( + GUIUtils.DrawRectangle( new Rect(weaponButtonRect.x, weaponButtonRect.y + weaponButtonRect.height, weaponButtonRect.width, 1), Color.white); } @@ -1204,200 +1325,143 @@ void WindowBDAToolbar(int windowID) if (showGuardMenu && !toolMinimized) { line += 0.25f; - GUI.BeginGroup( - new Rect(5, contentTop + (line * entryHeight), columnWidth - 10, (guardHeight) * entryHeight), - GUIContent.none, BDGuiSkin.box); + GUI.BeginGroup(new Rect(5, contentTop + line * entryHeight, columnWidth - 10, guardHeight * entryHeight), GUIContent.none, BDGuiSkin.box); guardLines += 0.1f; - contentWidth -= 16; - leftIndent += 3; - string guardButtonLabel = Localizer.Format("#LOC_BDArmory_WMWindow_NoneWeapon", (ActiveWeaponManager.guardMode ? Localizer.Format("#LOC_BDArmory_Generic_On") : Localizer.Format("#LOC_BDArmory_Generic_Off")));//"Guard Mode " + "ON""Off" - if (GUI.Button(new Rect(leftIndent, (guardLines * entryHeight), contentWidth, entryHeight), - guardButtonLabel, ActiveWeaponManager.guardMode ? BDGuiSkin.box : BDGuiSkin.button)) + string guardButtonLabel = StringUtils.Localize("#LOC_BDArmory_WMWindow_NoneWeapon", (ActiveWeaponManager.guardMode ? StringUtils.Localize("#LOC_BDArmory_Generic_On") : StringUtils.Localize("#LOC_BDArmory_Generic_Off")));//"Guard Mode " + "ON""Off" + if (GUI.Button(ButtonRect(guardLines), guardButtonLabel, ActiveWeaponManager.guardMode ? BDGuiSkin.box : BDGuiSkin.button)) { ActiveWeaponManager.ToggleGuardMode(); } - guardLines += 1.25f; + guardLines += 0.25f; - GUI.Label(new Rect(leftIndent, (guardLines * entryHeight), 85, entryHeight), Localizer.Format("#LOC_BDArmory_WMWindow_FiringInterval"), leftLabel);//"Firing Interval" + GUI.Label(LabelRect(++guardLines, guardLabelWidth), StringUtils.Localize("#LOC_BDArmory_WMWindow_FiringInterval"), leftLabel);//"Firing Interval" if (!NumFieldsEnabled) { - ActiveWeaponManager.targetScanInterval = - GUI.HorizontalSlider( - new Rect(leftIndent + (90), (guardLines * entryHeight), contentWidth - 90 - 38, entryHeight), - ActiveWeaponManager.targetScanInterval, 0.5f, 60f); - ActiveWeaponManager.targetScanInterval = Mathf.Round(ActiveWeaponManager.targetScanInterval * 2f) / 2f; + ActiveWeaponManager.targetScanInterval = BDAMath.RoundToUnit(GUI.HorizontalSlider(SliderRect(guardLines, guardLabelWidth), ActiveWeaponManager.targetScanInterval, 0.5f, 60f), 0.5f); + GUI.Label(RightLabelRect(guardLines), ActiveWeaponManager.targetScanInterval.ToString(), leftLabel); } else { - textNumFields["targetScanInterval"].tryParseValue(GUI.TextField(new Rect(leftIndent + (90), (guardLines * entryHeight), contentWidth - 90 - 38, entryHeight), textNumFields["targetScanInterval"].possibleValue, 4)); + textNumFields["targetScanInterval"].tryParseValue(GUI.TextField(InputFieldRect(guardLines, guardLabelWidth), textNumFields["targetScanInterval"].possibleValue, 4, inputFieldStyle)); ActiveWeaponManager.targetScanInterval = (float)textNumFields["targetScanInterval"].currentValue; } - GUI.Label(new Rect(leftIndent + (contentWidth - 35), (guardLines * entryHeight), 35, entryHeight), - ActiveWeaponManager.targetScanInterval.ToString(), leftLabel); - guardLines++; - // extension for feature_engagementenvelope: set the firing burst length - string burstLabel = Localizer.Format("#LOC_BDArmory_WMWindow_BurstLength");//"Burst Length" - GUI.Label(new Rect(leftIndent, (guardLines * entryHeight), 85, entryHeight), burstLabel, leftLabel); + string burstLabel = StringUtils.Localize("#LOC_BDArmory_WMWindow_BurstLength");//"Burst Length" + GUI.Label(LabelRect(++guardLines, guardLabelWidth), burstLabel, leftLabel); if (!NumFieldsEnabled) { - ActiveWeaponManager.fireBurstLength = - GUI.HorizontalSlider( - new Rect(leftIndent + (90), (guardLines * entryHeight), contentWidth - 90 - 38, entryHeight), - ActiveWeaponManager.fireBurstLength, 0, 10); - ActiveWeaponManager.fireBurstLength = Mathf.Round(ActiveWeaponManager.fireBurstLength * 20f) / 20f; + ActiveWeaponManager.fireBurstLength = BDAMath.RoundToUnit(GUI.HorizontalSlider(SliderRect(guardLines, guardLabelWidth), ActiveWeaponManager.fireBurstLength, 0, 10), 0.05f); + GUI.Label(RightLabelRect(guardLines), ActiveWeaponManager.fireBurstLength.ToString(), leftLabel); } else { - textNumFields["fireBurstLength"].tryParseValue(GUI.TextField(new Rect(leftIndent + (90), (guardLines * entryHeight), contentWidth - 90 - 38, entryHeight), textNumFields["fireBurstLength"].possibleValue, 4)); + textNumFields["fireBurstLength"].tryParseValue(GUI.TextField(InputFieldRect(guardLines, guardLabelWidth), textNumFields["fireBurstLength"].possibleValue, 4, inputFieldStyle)); ActiveWeaponManager.fireBurstLength = (float)textNumFields["fireBurstLength"].currentValue; } - GUI.Label(new Rect(leftIndent + (contentWidth - 35), (guardLines * entryHeight), 35, entryHeight), - ActiveWeaponManager.fireBurstLength.ToString(), leftLabel); - guardLines++; // extension for feature_engagementenvelope: set the firing accuracy tolarance var oldAutoFireCosAngleAdjustment = ActiveWeaponManager.AutoFireCosAngleAdjustment; - string accuracyLabel = Localizer.Format("#LOC_BDArmory_WMWindow_FiringTolerance");//"Firing Angle" - GUI.Label(new Rect(leftIndent, (guardLines * entryHeight), 85, entryHeight), accuracyLabel, leftLabel); + string accuracyLabel = StringUtils.Localize("#LOC_BDArmory_WMWindow_FiringTolerance");//"Firing Angle" + GUI.Label(LabelRect(++guardLines, guardLabelWidth), accuracyLabel, leftLabel); if (!NumFieldsEnabled) { - ActiveWeaponManager.AutoFireCosAngleAdjustment = - GUI.HorizontalSlider( - new Rect(leftIndent + (90), (guardLines * entryHeight), contentWidth - 90 - 38, entryHeight), - ActiveWeaponManager.AutoFireCosAngleAdjustment, 0, 4); - ActiveWeaponManager.AutoFireCosAngleAdjustment = Mathf.Round(ActiveWeaponManager.AutoFireCosAngleAdjustment * 20f) / 20f; + ActiveWeaponManager.AutoFireCosAngleAdjustment = BDAMath.RoundToUnit(GUI.HorizontalSlider(SliderRect(guardLines, guardLabelWidth), ActiveWeaponManager.AutoFireCosAngleAdjustment, 0, 4), 0.05f); + GUI.Label(RightLabelRect(guardLines), ActiveWeaponManager.AutoFireCosAngleAdjustment.ToString(), leftLabel); } else { - textNumFields["AutoFireCosAngleAdjustment"].tryParseValue(GUI.TextField(new Rect(leftIndent + (90), (guardLines * entryHeight), contentWidth - 90 - 38, entryHeight), textNumFields["AutoFireCosAngleAdjustment"].possibleValue, 4)); + textNumFields["AutoFireCosAngleAdjustment"].tryParseValue(GUI.TextField(InputFieldRect(guardLines, guardLabelWidth), textNumFields["AutoFireCosAngleAdjustment"].possibleValue, 4, inputFieldStyle)); ActiveWeaponManager.AutoFireCosAngleAdjustment = (float)textNumFields["AutoFireCosAngleAdjustment"].currentValue; } if (ActiveWeaponManager.AutoFireCosAngleAdjustment != oldAutoFireCosAngleAdjustment) ActiveWeaponManager.OnAFCAAUpdated(null, null); - GUI.Label(new Rect(leftIndent + (contentWidth - 35), (guardLines * entryHeight), 35, entryHeight), - ActiveWeaponManager.AutoFireCosAngleAdjustment.ToString(), leftLabel); - guardLines++; - GUI.Label(new Rect(leftIndent, (guardLines * entryHeight), 85, entryHeight), Localizer.Format("#LOC_BDArmory_WMWindow_FieldofView"),//"Field of View" + GUI.Label(LabelRect(++guardLines, guardLabelWidth), StringUtils.Localize("#LOC_BDArmory_WMWindow_FieldofView"),//"Field of View" leftLabel); if (!NumFieldsEnabled) { - float guardAngle = ActiveWeaponManager.guardAngle; - guardAngle = - GUI.HorizontalSlider( - new Rect(leftIndent + 90, (guardLines * entryHeight), contentWidth - 90 - 38, entryHeight), - guardAngle, 10, 360); - guardAngle = guardAngle / 10f; - guardAngle = Mathf.Round(guardAngle); - ActiveWeaponManager.guardAngle = guardAngle * 10f; + ActiveWeaponManager.guardAngle = BDAMath.RoundToUnit(GUI.HorizontalSlider(SliderRect(guardLines, guardLabelWidth), ActiveWeaponManager.guardAngle, 10, 360), 0.1f); + GUI.Label(RightLabelRect(guardLines), ActiveWeaponManager.guardAngle.ToString(), leftLabel); } else { - textNumFields["guardAngle"].tryParseValue(GUI.TextField(new Rect(leftIndent + (90), (guardLines * entryHeight), contentWidth - 90 - 38, entryHeight), textNumFields["guardAngle"].possibleValue, 4)); + textNumFields["guardAngle"].tryParseValue(GUI.TextField(InputFieldRect(guardLines, guardLabelWidth), textNumFields["guardAngle"].possibleValue, 4, inputFieldStyle)); ActiveWeaponManager.guardAngle = (float)textNumFields["guardAngle"].currentValue; } - GUI.Label(new Rect(leftIndent + (contentWidth - 35), (guardLines * entryHeight), 35, entryHeight), - ActiveWeaponManager.guardAngle.ToString(), leftLabel); - guardLines++; - GUI.Label(new Rect(leftIndent, (guardLines * entryHeight), 85, entryHeight), Localizer.Format("#LOC_BDArmory_WMWindow_VisualRange"), leftLabel);//"Visual Range" + GUI.Label(LabelRect(++guardLines, guardLabelWidth), StringUtils.Localize("#LOC_BDArmory_WMWindow_VisualRange"), leftLabel);//"Visual Range" if (!NumFieldsEnabled) { - float guardRange = ActiveWeaponManager.guardRange; - guardRange = - GUI.HorizontalSlider( - new Rect(leftIndent + 90, (guardLines * entryHeight), contentWidth - 90 - 38, entryHeight), - guardRange, 100, BDArmorySettings.MAX_GUARD_VISUAL_RANGE); - guardRange = guardRange / 100; - guardRange = Mathf.Round(guardRange); - ActiveWeaponManager.guardRange = guardRange * 100; + ActiveWeaponManager.guardRange = BDAMath.RoundToUnit(GUI.HorizontalSlider(SliderRect(guardLines, guardLabelWidth), ActiveWeaponManager.guardRange, 100, BDArmorySettings.MAX_GUARD_VISUAL_RANGE), 0.01f); + GUI.Label(RightLabelRect(guardLines), ActiveWeaponManager.guardRange.ToString(), leftLabel); } else { - textNumFields["guardRange"].tryParseValue(GUI.TextField(new Rect(leftIndent + (90), (guardLines * entryHeight), contentWidth - 90 - 38, entryHeight), textNumFields["guardRange"].possibleValue, 8)); + textNumFields["guardRange"].tryParseValue(GUI.TextField(InputFieldRect(guardLines, guardLabelWidth), textNumFields["guardRange"].possibleValue, 8, inputFieldStyle)); ActiveWeaponManager.guardRange = (float)textNumFields["guardRange"].currentValue; } - GUI.Label(new Rect(leftIndent + (contentWidth - 35), (guardLines * entryHeight), 35, entryHeight), - ActiveWeaponManager.guardRange.ToString(), leftLabel); - guardLines++; - GUI.Label(new Rect(leftIndent, (guardLines * entryHeight), 85, entryHeight), Localizer.Format("#LOC_BDArmory_WMWindow_GunsRange"), leftLabel);//"Guns Range" + GUI.Label(LabelRect(++guardLines, guardLabelWidth), StringUtils.Localize("#LOC_BDArmory_WMWindow_GunsRange"), leftLabel);//"Guns Range" if (!NumFieldsEnabled) { - float gRange = ActiveWeaponManager.gunRange; - gRange = - GUI.HorizontalSlider( - new Rect(leftIndent + 90, (guardLines * entryHeight), contentWidth - 90 - 38, entryHeight), - gRange, 0, ActiveWeaponManager.maxGunRange); - gRange /= 10f; - gRange = Mathf.Round(gRange); - gRange *= 10f; - ActiveWeaponManager.gunRange = gRange; + ActiveWeaponManager.gunRange = BDAMath.RoundToUnit(GUI.HorizontalSlider(SliderRect(guardLines, guardLabelWidth), ActiveWeaponManager.gunRange, 0, ActiveWeaponManager.maxGunRange), 0.1f); + GUI.Label(RightLabelRect(guardLines), ActiveWeaponManager.gunRange.ToString(), leftLabel); } else { - textNumFields["gunRange"].tryParseValue(GUI.TextField(new Rect(leftIndent + (90), (guardLines * entryHeight), contentWidth - 90 - 38, entryHeight), textNumFields["gunRange"].possibleValue, 8)); + textNumFields["gunRange"].tryParseValue(GUI.TextField(InputFieldRect(guardLines, guardLabelWidth), textNumFields["gunRange"].possibleValue, 8, inputFieldStyle)); ActiveWeaponManager.gunRange = (float)textNumFields["gunRange"].currentValue; } - GUI.Label(new Rect(leftIndent + (contentWidth - 35), (guardLines * entryHeight), 35, entryHeight), - ActiveWeaponManager.gunRange.ToString(), leftLabel); - guardLines++; - GUI.Label(new Rect(leftIndent, (guardLines * entryHeight), 85, entryHeight), Localizer.Format("#LOC_BDArmory_WMWindow_MultiTargetNum"), leftLabel);//"Max Turret targets " + GUI.Label(LabelRect(++guardLines, guardLabelWidth), StringUtils.Localize("#LOC_BDArmory_WMWindow_MultiTargetNum"), leftLabel);//"Max Turret targets " if (!NumFieldsEnabled) { - ActiveWeaponManager.multiTargetNum = - GUI.HorizontalSlider( - new Rect(leftIndent + 90, (guardLines * entryHeight), contentWidth - 90 - 38, entryHeight), - ActiveWeaponManager.multiTargetNum, 1, 10); - ActiveWeaponManager.multiTargetNum = Mathf.Round(ActiveWeaponManager.multiTargetNum); + ActiveWeaponManager.multiTargetNum = Mathf.Round(GUI.HorizontalSlider(SliderRect(guardLines, guardLabelWidth), ActiveWeaponManager.multiTargetNum, 1, 10)); + GUI.Label(RightLabelRect(guardLines), ActiveWeaponManager.multiTargetNum.ToString(), leftLabel); } else { - textNumFields["multiTargetNum"].tryParseValue(GUI.TextField(new Rect(leftIndent + (90), (guardLines * entryHeight), contentWidth - 90 - 38, entryHeight), textNumFields["multiTargetNum"].possibleValue, 2)); + textNumFields["multiTargetNum"].tryParseValue(GUI.TextField(InputFieldRect(guardLines, guardLabelWidth), textNumFields["multiTargetNum"].possibleValue, 2, inputFieldStyle)); ActiveWeaponManager.multiTargetNum = (float)textNumFields["multiTargetNum"].currentValue; } - GUI.Label(new Rect(leftIndent + (contentWidth - 35), (guardLines * entryHeight), 35, entryHeight), - ActiveWeaponManager.multiTargetNum.ToString(), leftLabel); - guardLines++; - GUI.Label(new Rect(leftIndent, (guardLines * entryHeight), 85, entryHeight), Localizer.Format("#LOC_BDArmory_WMWindow_MissilesTgt"), leftLabel);//"Missiles/Tgt" + GUI.Label(LabelRect(++guardLines, guardLabelWidth), StringUtils.Localize("#LOC_BDArmory_WMWindow_MultiMissileNum"), leftLabel);//"Max Turret targets " if (!NumFieldsEnabled) { - float mslCount = ActiveWeaponManager.maxMissilesOnTarget; - mslCount = - GUI.HorizontalSlider( - new Rect(leftIndent + 90, (guardLines * entryHeight), contentWidth - 90 - 38, entryHeight), - mslCount, 1, MissileFire.maxAllowableMissilesOnTarget); - mslCount = Mathf.Round(mslCount); - ActiveWeaponManager.maxMissilesOnTarget = mslCount; + ActiveWeaponManager.multiMissileTgtNum = Mathf.Round(GUI.HorizontalSlider(SliderRect(guardLines, guardLabelWidth), ActiveWeaponManager.multiMissileTgtNum, 1, 10)); + GUI.Label(RightLabelRect(guardLines), ActiveWeaponManager.multiMissileTgtNum.ToString(), leftLabel); } else { - textNumFields["maxMissilesOnTarget"].tryParseValue(GUI.TextField(new Rect(leftIndent + (90), (guardLines * entryHeight), contentWidth - 90 - 38, entryHeight), textNumFields["maxMissilesOnTarget"].possibleValue, 2)); + textNumFields["multiMissileTgtNum"].tryParseValue(GUI.TextField(InputFieldRect(guardLines, guardLabelWidth), textNumFields["multiMissileTgtNum"].possibleValue, 2, inputFieldStyle)); + ActiveWeaponManager.multiMissileTgtNum = (float)textNumFields["multiMissileTgtNum"].currentValue; + } + + GUI.Label(LabelRect(++guardLines, guardLabelWidth), StringUtils.Localize("#LOC_BDArmory_WMWindow_MissilesTgt"), leftLabel);//"Missiles/Tgt" + if (!NumFieldsEnabled) + { + ActiveWeaponManager.maxMissilesOnTarget = Mathf.Round(GUI.HorizontalSlider(SliderRect(guardLines, guardLabelWidth), ActiveWeaponManager.maxMissilesOnTarget, 1, MissileFire.maxAllowableMissilesOnTarget)); + GUI.Label(RightLabelRect(guardLines), ActiveWeaponManager.maxMissilesOnTarget.ToString(), leftLabel); + } + else + { + textNumFields["maxMissilesOnTarget"].tryParseValue(GUI.TextField(InputFieldRect(guardLines, guardLabelWidth), textNumFields["maxMissilesOnTarget"].possibleValue, 2, inputFieldStyle)); ActiveWeaponManager.maxMissilesOnTarget = (float)textNumFields["maxMissilesOnTarget"].currentValue; } - GUI.Label(new Rect(leftIndent + (contentWidth - 35), (guardLines * entryHeight), 35, entryHeight), - ActiveWeaponManager.maxMissilesOnTarget.ToString(), leftLabel); - guardLines += 0.5f; - showTargetOptions = GUI.Toggle(new Rect(leftIndent, contentTop + (guardLines * entryHeight), columnWidth - (2 * leftIndent), entryHeight), - showTargetOptions, Localizer.Format("#LOC_BDArmory_Settings_Adv_Targeting"), showTargetOptions ? BDGuiSkin.box : BDGuiSkin.button);//"Advanced Targeting" - guardLines += 1.15f; + showTargetOptions = GUI.Toggle(ButtonRect(++guardLines), showTargetOptions, StringUtils.Localize("#LOC_BDArmory_Settings_Adv_Targeting"), showTargetOptions ? BDGuiSkin.box : BDGuiSkin.button);//"Advanced Targeting" + guardLines += 0.25f; float TargetLines = 0; if (showTargetOptions && showGuardMenu && !toolMinimized) { - TargetLines += 0.1f; - GUI.BeginGroup( - new Rect(5, contentTop + (guardLines * entryHeight), columnWidth - 10, TargetingHeight * entryHeight), - GUIContent.none, BDGuiSkin.box); + contentWidth = columnWidth - 30; + guardLines += 0.25f; + GUI.BeginGroup(new Rect(10, contentTop + (guardLines * entryHeight), contentWidth, (TargetingHeight + 0.25f) * entryHeight), GUIContent.none, BDGuiSkin.box); TargetLines += 0.25f; - string CoMlabel = Localizer.Format("#LOC_BDArmory_TargetCOM", (ActiveWeaponManager.targetCoM ? Localizer.Format("#LOC_BDArmory_false") : Localizer.Format("#LOC_BDArmory_true")));//"Engage Air; True, False - if (GUI.Button(new Rect(leftIndent, (TargetLines * entryHeight), (contentWidth - (2 * leftIndent)), entryHeight), - CoMlabel, ActiveWeaponManager.targetCoM ? BDGuiSkin.box : BDGuiSkin.button)) + string CoMlabel = StringUtils.Localize("#LOC_BDArmory_TargetCOM", (ActiveWeaponManager.targetCoM ? StringUtils.Localize("#LOC_BDArmory_false") : StringUtils.Localize("#LOC_BDArmory_true")));//"Engage Air; True, False + if (GUI.Button(new Rect(leftIndent, TargetLines * entryHeight, contentWidth - 2 * leftIndent, entryHeight), CoMlabel, ActiveWeaponManager.targetCoM ? BDGuiSkin.box : BDGuiSkin.button)) { ActiveWeaponManager.targetCoM = !ActiveWeaponManager.targetCoM; ActiveWeaponManager.StartGuardTurretFiring(); //reset weapon targeting assignments @@ -1407,16 +1471,16 @@ void WindowBDAToolbar(int windowID) ActiveWeaponManager.targetEngine = false; ActiveWeaponManager.targetWeapon = false; ActiveWeaponManager.targetMass = false; + ActiveWeaponManager.targetRandom = false; } - if (!ActiveWeaponManager.targetCoM && (!ActiveWeaponManager.targetWeapon && !ActiveWeaponManager.targetEngine && !ActiveWeaponManager.targetCommand && !ActiveWeaponManager.targetMass)) + if (!ActiveWeaponManager.targetCoM && (!ActiveWeaponManager.targetWeapon && !ActiveWeaponManager.targetEngine && !ActiveWeaponManager.targetCommand && !ActiveWeaponManager.targetMass && !ActiveWeaponManager.targetRandom)) { - ActiveWeaponManager.targetMass = true; + ActiveWeaponManager.targetRandom = true; } } TargetLines += 1.1f; - string Commandlabel = Localizer.Format("#LOC_BDArmory_Command", (ActiveWeaponManager.targetCommand ? Localizer.Format("#LOC_BDArmory_false") : Localizer.Format("#LOC_BDArmory_true")));//"Engage Air; True, False - if (GUI.Button(new Rect(leftIndent, (TargetLines * entryHeight), ((contentWidth - (2 * leftIndent)) / 2), entryHeight), - Commandlabel, ActiveWeaponManager.targetCommand ? BDGuiSkin.box : BDGuiSkin.button)) + string Commandlabel = StringUtils.Localize("#LOC_BDArmory_Command", (ActiveWeaponManager.targetCommand ? StringUtils.Localize("#LOC_BDArmory_false") : StringUtils.Localize("#LOC_BDArmory_true")));//"Engage Air; True, False + if (GUI.Button(new Rect(leftIndent, TargetLines * entryHeight, (contentWidth - 2 * leftIndent) / 2, entryHeight), Commandlabel, ActiveWeaponManager.targetCommand ? BDGuiSkin.box : BDGuiSkin.button)) { ActiveWeaponManager.targetCommand = !ActiveWeaponManager.targetCommand; ActiveWeaponManager.StartGuardTurretFiring(); @@ -1424,14 +1488,13 @@ void WindowBDAToolbar(int windowID) { ActiveWeaponManager.targetCoM = false; } - if (!ActiveWeaponManager.targetCoM && (!ActiveWeaponManager.targetWeapon && !ActiveWeaponManager.targetEngine && !ActiveWeaponManager.targetCommand && !ActiveWeaponManager.targetMass)) + if (!ActiveWeaponManager.targetCoM && (!ActiveWeaponManager.targetWeapon && !ActiveWeaponManager.targetEngine && !ActiveWeaponManager.targetCommand && !ActiveWeaponManager.targetMass && !ActiveWeaponManager.targetRandom)) { ActiveWeaponManager.targetCoM = true; } } - string Engineslabel = Localizer.Format("#LOC_BDArmory_Engines", (ActiveWeaponManager.targetEngine ? Localizer.Format("#LOC_BDArmory_false") : Localizer.Format("#LOC_BDArmory_true")));//"Engage Missile; True, False - if (GUI.Button(new Rect(leftIndent + ((contentWidth - (2 * leftIndent)) / 2), (TargetLines * entryHeight), ((contentWidth - (2 * leftIndent)) / 2), entryHeight), - Engineslabel, ActiveWeaponManager.targetEngine ? BDGuiSkin.box : BDGuiSkin.button)) + string Engineslabel = StringUtils.Localize("#LOC_BDArmory_Engines", (ActiveWeaponManager.targetEngine ? StringUtils.Localize("#LOC_BDArmory_false") : StringUtils.Localize("#LOC_BDArmory_true")));//"Engage Missile; True, False + if (GUI.Button(new Rect(leftIndent + (contentWidth - 2 * leftIndent) / 2, TargetLines * entryHeight, (contentWidth - 2 * leftIndent) / 2, entryHeight), Engineslabel, ActiveWeaponManager.targetEngine ? BDGuiSkin.box : BDGuiSkin.button)) { ActiveWeaponManager.targetEngine = !ActiveWeaponManager.targetEngine; ActiveWeaponManager.StartGuardTurretFiring(); @@ -1439,15 +1502,14 @@ void WindowBDAToolbar(int windowID) { ActiveWeaponManager.targetCoM = false; } - if (!ActiveWeaponManager.targetCoM && (!ActiveWeaponManager.targetWeapon && !ActiveWeaponManager.targetEngine && !ActiveWeaponManager.targetCommand && !ActiveWeaponManager.targetMass)) + if (!ActiveWeaponManager.targetCoM && (!ActiveWeaponManager.targetWeapon && !ActiveWeaponManager.targetEngine && !ActiveWeaponManager.targetCommand && !ActiveWeaponManager.targetMass && !ActiveWeaponManager.targetRandom)) { ActiveWeaponManager.targetCoM = true; } } TargetLines += 1.1f; - string Weaponslabel = Localizer.Format("#LOC_BDArmory_Weapons", (ActiveWeaponManager.targetWeapon ? Localizer.Format("#LOC_BDArmory_false") : Localizer.Format("#LOC_BDArmory_true")));//"Engage Surface; True, False - if (GUI.Button(new Rect(leftIndent, (TargetLines * entryHeight), ((contentWidth - (2 * leftIndent)) / 2), entryHeight), - Weaponslabel, ActiveWeaponManager.targetWeapon ? BDGuiSkin.box : BDGuiSkin.button)) + string Weaponslabel = StringUtils.Localize("#LOC_BDArmory_Weapons", (ActiveWeaponManager.targetWeapon ? StringUtils.Localize("#LOC_BDArmory_false") : StringUtils.Localize("#LOC_BDArmory_true")));//"Engage Surface; True, False + if (GUI.Button(new Rect(leftIndent, TargetLines * entryHeight, (contentWidth - 2 * leftIndent) / 2, entryHeight), Weaponslabel, ActiveWeaponManager.targetWeapon ? BDGuiSkin.box : BDGuiSkin.button)) { ActiveWeaponManager.targetWeapon = !ActiveWeaponManager.targetWeapon; ActiveWeaponManager.StartGuardTurretFiring(); @@ -1455,14 +1517,13 @@ void WindowBDAToolbar(int windowID) { ActiveWeaponManager.targetCoM = false; } - if (!ActiveWeaponManager.targetCoM && (!ActiveWeaponManager.targetWeapon && !ActiveWeaponManager.targetEngine && !ActiveWeaponManager.targetCommand && !ActiveWeaponManager.targetMass)) + if (!ActiveWeaponManager.targetCoM && (!ActiveWeaponManager.targetWeapon && !ActiveWeaponManager.targetEngine && !ActiveWeaponManager.targetCommand && !ActiveWeaponManager.targetMass && !ActiveWeaponManager.targetRandom)) { ActiveWeaponManager.targetCoM = true; } } - string Masslabel = Localizer.Format("#LOC_BDArmory_Mass", (ActiveWeaponManager.targetMass ? Localizer.Format("#LOC_BDArmory_false") : Localizer.Format("#LOC_BDArmory_true")));//"Engage SLW; True, False - if (GUI.Button(new Rect(leftIndent + ((contentWidth - (2 * leftIndent)) / 2), (TargetLines * entryHeight), ((contentWidth - (2 * leftIndent)) / 2), entryHeight), - Masslabel, ActiveWeaponManager.targetMass ? BDGuiSkin.box : BDGuiSkin.button)) + string Masslabel = StringUtils.Localize("#LOC_BDArmory_Mass", (ActiveWeaponManager.targetMass ? StringUtils.Localize("#LOC_BDArmory_false") : StringUtils.Localize("#LOC_BDArmory_true")));//"Engage SLW; True, False + if (GUI.Button(new Rect(leftIndent + (contentWidth - 2 * leftIndent) / 2, TargetLines * entryHeight, (contentWidth - 2 * leftIndent) / 2, entryHeight), Masslabel, ActiveWeaponManager.targetMass ? BDGuiSkin.box : BDGuiSkin.button)) { ActiveWeaponManager.targetMass = !ActiveWeaponManager.targetMass; ActiveWeaponManager.StartGuardTurretFiring(); @@ -1470,75 +1531,78 @@ void WindowBDAToolbar(int windowID) { ActiveWeaponManager.targetCoM = false; } - if (!ActiveWeaponManager.targetCoM && (!ActiveWeaponManager.targetWeapon && !ActiveWeaponManager.targetEngine && !ActiveWeaponManager.targetCommand && !ActiveWeaponManager.targetMass)) + if (!ActiveWeaponManager.targetCoM && (!ActiveWeaponManager.targetWeapon && !ActiveWeaponManager.targetEngine && !ActiveWeaponManager.targetCommand && !ActiveWeaponManager.targetMass && !ActiveWeaponManager.targetRandom)) { ActiveWeaponManager.targetCoM = true; } } TargetLines += 1.1f; - - ActiveWeaponManager.targetingString = (ActiveWeaponManager.targetCoM ? Localizer.Format("#LOC_BDArmory_TargetCOM") + "; " : "") - + (ActiveWeaponManager.targetMass ? Localizer.Format("#LOC_BDArmory_Mass") + "; " : "") - + (ActiveWeaponManager.targetCommand ? Localizer.Format("#LOC_BDArmory_Command") + "; " : "") - + (ActiveWeaponManager.targetEngine ? Localizer.Format("#LOC_BDArmory_Engines") + "; " : "") - + (ActiveWeaponManager.targetWeapon ? Localizer.Format("#LOC_BDArmory_Weapons") + "; " : ""); + string Randomlabel = StringUtils.Localize("#LOC_BDArmory_Random", (ActiveWeaponManager.targetRandom ? StringUtils.Localize("#LOC_BDArmory_false") : StringUtils.Localize("#LOC_BDArmory_true")));//"Engage Surface; True, False + if (GUI.Button(new Rect(leftIndent, TargetLines * entryHeight, (contentWidth - 2 * leftIndent) / 2, entryHeight), Randomlabel, ActiveWeaponManager.targetRandom ? BDGuiSkin.box : BDGuiSkin.button)) + { + ActiveWeaponManager.targetRandom = !ActiveWeaponManager.targetRandom; + ActiveWeaponManager.StartGuardTurretFiring(); + if (ActiveWeaponManager.targetRandom) + { + ActiveWeaponManager.targetCoM = false; + } + if (!ActiveWeaponManager.targetCoM && (!ActiveWeaponManager.targetWeapon && !ActiveWeaponManager.targetEngine && !ActiveWeaponManager.targetCommand && !ActiveWeaponManager.targetMass && !ActiveWeaponManager.targetRandom)) + { + ActiveWeaponManager.targetCoM = true; + } + } + TargetLines += 1.1f; + ActiveWeaponManager.targetingString = (ActiveWeaponManager.targetCoM ? StringUtils.Localize("#LOC_BDArmory_TargetCOM") + "; " : "") + + (ActiveWeaponManager.targetMass ? StringUtils.Localize("#LOC_BDArmory_Mass") + "; " : "") + + (ActiveWeaponManager.targetCommand ? StringUtils.Localize("#LOC_BDArmory_Command") + "; " : "") + + (ActiveWeaponManager.targetEngine ? StringUtils.Localize("#LOC_BDArmory_Engines") + "; " : "") + + (ActiveWeaponManager.targetWeapon ? StringUtils.Localize("#LOC_BDArmory_Weapons") + "; " : "") + + (ActiveWeaponManager.targetWeapon ? StringUtils.Localize("#LOC_BDArmory_Random") + "; " : ""); GUI.EndGroup(); - TargetLines += 0.1f; } TargetingHeight = Mathf.Lerp(TargetingHeight, TargetLines, 0.15f); guardLines += TargetingHeight; - guardLines += 0.1f; - showEngageList = GUI.Toggle(new Rect(leftIndent, contentTop + (guardLines * entryHeight), columnWidth - (2 * leftIndent), entryHeight), - showEngageList, showEngageList ? Localizer.Format("#LOC_BDArmory_DisableEngageOptions") : Localizer.Format("#LOC_BDArmory_EnableEngageOptions"), showEngageList ? BDGuiSkin.box : BDGuiSkin.button);//"Enable/Disable Engagement options" - guardLines += 1.15f; + showEngageList = GUI.Toggle(ButtonRect(++guardLines), showEngageList, showEngageList ? StringUtils.Localize("#LOC_BDArmory_DisableEngageOptions") : StringUtils.Localize("#LOC_BDArmory_EnableEngageOptions"), showEngageList ? BDGuiSkin.box : BDGuiSkin.button);//"Enable/Disable Engagement options" + guardLines += 0.25f; float EngageLines = 0; if (showEngageList && showGuardMenu && !toolMinimized) { - EngageLines += 0.1f; - GUI.BeginGroup( - new Rect(5, contentTop + (guardLines * entryHeight), columnWidth - 10, EngageHeight * entryHeight), - GUIContent.none, BDGuiSkin.box); + contentWidth = columnWidth - 30; + guardLines += 0.25f; + GUI.BeginGroup(new Rect(10, contentTop + guardLines * entryHeight, contentWidth, (EngageHeight + 0.25f) * entryHeight), GUIContent.none, BDGuiSkin.box); EngageLines += 0.25f; - string Airlabel = Localizer.Format("#LOC_BDArmory_EngageAir", (ActiveWeaponManager.engageAir ? Localizer.Format("#LOC_BDArmory_false") : Localizer.Format("#LOC_BDArmory_true")));//"Engage Air; True, False - if (GUI.Button(new Rect(leftIndent, (EngageLines * entryHeight), ((contentWidth - (2 * leftIndent)) / 2), entryHeight), - Airlabel, ActiveWeaponManager.engageAir ? BDGuiSkin.box : BDGuiSkin.button)) + string Airlabel = StringUtils.Localize("#LOC_BDArmory_EngageAir", (ActiveWeaponManager.engageAir ? StringUtils.Localize("#LOC_BDArmory_false") : StringUtils.Localize("#LOC_BDArmory_true")));//"Engage Air; True, False + if (GUI.Button(new Rect(leftIndent, EngageLines * entryHeight, (contentWidth - 2 * leftIndent) / 2, entryHeight), Airlabel, ActiveWeaponManager.engageAir ? BDGuiSkin.box : BDGuiSkin.button)) { ActiveWeaponManager.ToggleEngageAir(); } - string Missilelabel = Localizer.Format("#LOC_BDArmory_EngageMissile", (ActiveWeaponManager.engageMissile ? Localizer.Format("#LOC_BDArmory_false") : Localizer.Format("#LOC_BDArmory_true")));//"Engage Missile; True, False - if (GUI.Button(new Rect(leftIndent + ((contentWidth - (2 * leftIndent)) / 2), (EngageLines * entryHeight), ((contentWidth - (2 * leftIndent)) / 2), entryHeight), - Missilelabel, ActiveWeaponManager.engageMissile ? BDGuiSkin.box : BDGuiSkin.button)) + string Missilelabel = StringUtils.Localize("#LOC_BDArmory_EngageMissile", (ActiveWeaponManager.engageMissile ? StringUtils.Localize("#LOC_BDArmory_false") : StringUtils.Localize("#LOC_BDArmory_true")));//"Engage Missile; True, False + if (GUI.Button(new Rect(leftIndent + (contentWidth - 2 * leftIndent) / 2, EngageLines * entryHeight, (contentWidth - 2 * leftIndent) / 2, entryHeight), Missilelabel, ActiveWeaponManager.engageMissile ? BDGuiSkin.box : BDGuiSkin.button)) { ActiveWeaponManager.ToggleEngageMissile(); } EngageLines += 1.1f; - string Srflabel = Localizer.Format("#LOC_BDArmory_EngageSurface", (ActiveWeaponManager.engageSrf ? Localizer.Format("#LOC_BDArmory_false") : Localizer.Format("#LOC_BDArmory_true")));//"Engage Surface; True, False - if (GUI.Button(new Rect(leftIndent, (EngageLines * entryHeight), ((contentWidth - (2 * leftIndent)) / 2), entryHeight), - Srflabel, ActiveWeaponManager.engageSrf ? BDGuiSkin.box : BDGuiSkin.button)) + string Srflabel = StringUtils.Localize("#LOC_BDArmory_EngageSurface", (ActiveWeaponManager.engageSrf ? StringUtils.Localize("#LOC_BDArmory_false") : StringUtils.Localize("#LOC_BDArmory_true")));//"Engage Surface; True, False + if (GUI.Button(new Rect(leftIndent, EngageLines * entryHeight, (contentWidth - 2 * leftIndent) / 2, entryHeight), Srflabel, ActiveWeaponManager.engageSrf ? BDGuiSkin.box : BDGuiSkin.button)) { ActiveWeaponManager.ToggleEngageSrf(); } - string SLWlabel = Localizer.Format("#LOC_BDArmory_EngageSLW", (ActiveWeaponManager.engageSLW ? Localizer.Format("#LOC_BDArmory_false") : Localizer.Format("#LOC_BDArmory_true")));//"Engage SLW; True, False - if (GUI.Button(new Rect(leftIndent + ((contentWidth - (2 * leftIndent)) / 2), (EngageLines * entryHeight), ((contentWidth - (2 * leftIndent)) / 2), entryHeight), - SLWlabel, ActiveWeaponManager.engageSLW ? BDGuiSkin.box : BDGuiSkin.button)) + string SLWlabel = StringUtils.Localize("#LOC_BDArmory_EngageSLW", (ActiveWeaponManager.engageSLW ? StringUtils.Localize("#LOC_BDArmory_false") : StringUtils.Localize("#LOC_BDArmory_true")));//"Engage SLW; True, False + if (GUI.Button(new Rect(leftIndent + (contentWidth - 2 * leftIndent) / 2, EngageLines * entryHeight, (contentWidth - 2 * leftIndent) / 2, entryHeight), SLWlabel, ActiveWeaponManager.engageSLW ? BDGuiSkin.box : BDGuiSkin.button)) { ActiveWeaponManager.ToggleEngageSLW(); } EngageLines += 1.1f; GUI.EndGroup(); - EngageLines += 0.1f; } EngageHeight = Mathf.Lerp(EngageHeight, EngageLines, 0.15f); guardLines += EngageHeight; - guardLines += 0.1f; - guardLines += 0.5f; - - guardLines += 0.1f; GUI.EndGroup(); + ++guardLines; } guardHeight = Mathf.Lerp(guardHeight, guardLines, 0.15f); line += guardHeight; @@ -1547,246 +1611,190 @@ void WindowBDAToolbar(int windowID) if (showPriorities && !toolMinimized) { line += 0.25f; - GUI.BeginGroup( - new Rect(5, contentTop + (line * entryHeight), columnWidth - 10, (priorityheight) * entryHeight), - GUIContent.none, BDGuiSkin.box); + GUI.BeginGroup(new Rect(5, contentTop + line * entryHeight, columnWidth - 10, priorityheight * entryHeight), GUIContent.none, BDGuiSkin.box); priorityLines += 0.1f; - GUI.Label(new Rect(leftIndent, (priorityLines * entryHeight), 85, entryHeight), Localizer.Format("#LOC_BDArmory_WMWindow_targetBias"), leftLabel);//"current target bias" + GUI.Label(LabelRect(++priorityLines, priorityLabelWidth), StringUtils.Localize("#LOC_BDArmory_WMWindow_targetBias"), leftLabel);//"current target bias" if (!NumFieldsEnabled) { - ActiveWeaponManager.targetBias = - GUI.HorizontalSlider( - new Rect(leftIndent + (150), (priorityLines * entryHeight), contentWidth - 150 - 38, entryHeight), - ActiveWeaponManager.targetBias, -10, 10); - ActiveWeaponManager.targetBias = Mathf.Round(ActiveWeaponManager.targetBias * 10f) / 10f; + ActiveWeaponManager.targetBias = BDAMath.RoundToUnit(GUI.HorizontalSlider(SliderRect(priorityLines, priorityLabelWidth), ActiveWeaponManager.targetBias, -10, 10), 0.1f); + GUI.Label(RightLabelRect(priorityLines), ActiveWeaponManager.targetBias.ToString(), leftLabel); } else { - textNumFields["targetBias"].tryParseValue(GUI.TextField(new Rect(leftIndent + (90), (priorityLines * entryHeight), contentWidth - 90 - 38, entryHeight), textNumFields["targetBias"].possibleValue, 4)); + textNumFields["targetBias"].tryParseValue(GUI.TextField(InputFieldRect(priorityLines, priorityLabelWidth), textNumFields["targetBias"].possibleValue, 4, inputFieldStyle)); ActiveWeaponManager.targetBias = (float)textNumFields["targetBias"].currentValue; } - GUI.Label(new Rect(leftIndent + (contentWidth - 35), (priorityLines * entryHeight), 35, entryHeight), - ActiveWeaponManager.targetBias.ToString(), leftLabel); - priorityLines++; - GUI.Label(new Rect(leftIndent, (priorityLines * entryHeight), 85, entryHeight), Localizer.Format("#LOC_BDArmory_WMWindow_targetProximity"), leftLabel); //target proximity" + GUI.Label(LabelRect(++priorityLines, priorityLabelWidth), StringUtils.Localize("#LOC_BDArmory_WMWindow_targetProximity"), leftLabel); //target proximity" if (!NumFieldsEnabled) { - ActiveWeaponManager.targetWeightRange = - GUI.HorizontalSlider( - new Rect(leftIndent + (150), (priorityLines * entryHeight), contentWidth - 150 - 38, entryHeight), - ActiveWeaponManager.targetWeightRange, -10, 10); - ActiveWeaponManager.targetWeightRange = Mathf.Round(ActiveWeaponManager.targetWeightRange * 10) / 10; + ActiveWeaponManager.targetWeightRange = BDAMath.RoundToUnit(GUI.HorizontalSlider(SliderRect(priorityLines, priorityLabelWidth), ActiveWeaponManager.targetWeightRange, -10, 10), 0.1f); + GUI.Label(RightLabelRect(priorityLines), ActiveWeaponManager.targetWeightRange.ToString(), leftLabel); } else { - textNumFields["targetWeightRange"].tryParseValue(GUI.TextField(new Rect(leftIndent + (90), (priorityLines * entryHeight), contentWidth - 90 - 38, entryHeight), textNumFields["targetWeightRange"].possibleValue, 4)); + textNumFields["targetWeightRange"].tryParseValue(GUI.TextField(InputFieldRect(priorityLines, priorityLabelWidth), textNumFields["targetWeightRange"].possibleValue, 4, inputFieldStyle)); ActiveWeaponManager.targetWeightRange = (float)textNumFields["targetWeightRange"].currentValue; } - GUI.Label(new Rect(leftIndent + (contentWidth - 35), (priorityLines * entryHeight), 35, entryHeight), - ActiveWeaponManager.targetWeightRange.ToString(), leftLabel); - priorityLines++; - GUI.Label(new Rect(leftIndent, (priorityLines * entryHeight), 85, entryHeight), Localizer.Format("#LOC_BDArmory_WMWindow_targetAngletoTarget"), leftLabel); //target proximity" + GUI.Label(LabelRect(++priorityLines, priorityLabelWidth), StringUtils.Localize("#LOC_BDArmory_WMWindow_targetPreference"), leftLabel); //target Air preference" if (!NumFieldsEnabled) { - ActiveWeaponManager.targetWeightATA = - GUI.HorizontalSlider( - new Rect(leftIndent + (150), (priorityLines * entryHeight), contentWidth - 150 - 38, entryHeight), - ActiveWeaponManager.targetWeightATA, -10, 10); - ActiveWeaponManager.targetWeightATA = Mathf.Round(ActiveWeaponManager.targetWeightATA * 10) / 10; + ActiveWeaponManager.targetWeightAirPreference = BDAMath.RoundToUnit(GUI.HorizontalSlider(SliderRect(priorityLines, priorityLabelWidth), ActiveWeaponManager.targetWeightAirPreference, -10, 10), 0.1f); + GUI.Label(RightLabelRect(priorityLines), ActiveWeaponManager.targetWeightAirPreference.ToString(), leftLabel); } else { - textNumFields["targetWeightATA"].tryParseValue(GUI.TextField(new Rect(leftIndent + (90), (priorityLines * entryHeight), contentWidth - 90 - 38, entryHeight), textNumFields["targetWeightATA"].possibleValue, 4)); + textNumFields["targetWeightAirPreference"].tryParseValue(GUI.TextField(InputFieldRect(priorityLines, priorityLabelWidth), textNumFields["targetWeightAirPreference"].possibleValue, 4, inputFieldStyle)); + ActiveWeaponManager.targetWeightAirPreference = (float)textNumFields["targetWeightAirPreference"].currentValue; + } + + GUI.Label(LabelRect(++priorityLines, priorityLabelWidth), StringUtils.Localize("#LOC_BDArmory_WMWindow_targetAngletoTarget"), leftLabel); //target angle" + if (!NumFieldsEnabled) + { + ActiveWeaponManager.targetWeightATA = BDAMath.RoundToUnit(GUI.HorizontalSlider(SliderRect(priorityLines, priorityLabelWidth), ActiveWeaponManager.targetWeightATA, -10, 10), 0.1f); + GUI.Label(RightLabelRect(priorityLines), ActiveWeaponManager.targetWeightATA.ToString(), leftLabel); + } + else + { + textNumFields["targetWeightATA"].tryParseValue(GUI.TextField(InputFieldRect(priorityLines, priorityLabelWidth), textNumFields["targetWeightATA"].possibleValue, 4, inputFieldStyle)); ActiveWeaponManager.targetWeightATA = (float)textNumFields["targetWeightATA"].currentValue; } - GUI.Label(new Rect(leftIndent + (contentWidth - 35), (priorityLines * entryHeight), 35, entryHeight), - ActiveWeaponManager.targetWeightATA.ToString(), leftLabel); - priorityLines++; - GUI.Label(new Rect(leftIndent, (priorityLines * entryHeight), 85, entryHeight), Localizer.Format("#LOC_BDArmory_WMWindow_targetAngleDist"), leftLabel); //target proximity" + GUI.Label(LabelRect(++priorityLines, priorityLabelWidth), StringUtils.Localize("#LOC_BDArmory_WMWindow_targetAngleDist"), leftLabel); //Angle over Distance" if (!NumFieldsEnabled) { - ActiveWeaponManager.targetWeightAoD = - GUI.HorizontalSlider( - new Rect(leftIndent + (150), (priorityLines * entryHeight), contentWidth - 150 - 38, entryHeight), - ActiveWeaponManager.targetWeightAoD, -10, 10); - ActiveWeaponManager.targetWeightAoD = Mathf.Round(ActiveWeaponManager.targetWeightAoD * 10) / 10; + ActiveWeaponManager.targetWeightAoD = BDAMath.RoundToUnit(GUI.HorizontalSlider(SliderRect(priorityLines, priorityLabelWidth), ActiveWeaponManager.targetWeightAoD, -10, 10), 0.1f); + GUI.Label(RightLabelRect(priorityLines), ActiveWeaponManager.targetWeightAoD.ToString(), leftLabel); } else { - textNumFields["targetWeightAoD"].tryParseValue(GUI.TextField(new Rect(leftIndent + (90), (priorityLines * entryHeight), contentWidth - 90 - 38, entryHeight), textNumFields["targetWeightAoD"].possibleValue, 4)); + textNumFields["targetWeightAoD"].tryParseValue(GUI.TextField(InputFieldRect(priorityLines, priorityLabelWidth), textNumFields["targetWeightAoD"].possibleValue, 4, inputFieldStyle)); ActiveWeaponManager.targetWeightAoD = (float)textNumFields["targetWeightAoD"].currentValue; } - GUI.Label(new Rect(leftIndent + (contentWidth - 35), (priorityLines * entryHeight), 35, entryHeight), - ActiveWeaponManager.targetWeightAoD.ToString(), leftLabel); - priorityLines++; - GUI.Label(new Rect(leftIndent, (priorityLines * entryHeight), 85, entryHeight), Localizer.Format("#LOC_BDArmory_WMWindow_targetAccel"), leftLabel); //target proximity" + GUI.Label(LabelRect(++priorityLines, priorityLabelWidth), StringUtils.Localize("#LOC_BDArmory_WMWindow_targetAccel"), leftLabel); //target accel" if (!NumFieldsEnabled) { - ActiveWeaponManager.targetWeightAccel = - GUI.HorizontalSlider( - new Rect(leftIndent + (150), (priorityLines * entryHeight), contentWidth - 150 - 38, entryHeight), - ActiveWeaponManager.targetWeightAccel, -10, 10); - ActiveWeaponManager.targetWeightAccel = Mathf.Round(ActiveWeaponManager.targetWeightAccel * 10) / 10; + ActiveWeaponManager.targetWeightAccel = BDAMath.RoundToUnit(GUI.HorizontalSlider(SliderRect(priorityLines, priorityLabelWidth), ActiveWeaponManager.targetWeightAccel, -10, 10), 0.1f); + GUI.Label(RightLabelRect(priorityLines), ActiveWeaponManager.targetWeightAccel.ToString(), leftLabel); } else { - textNumFields["targetWeightAccel"].tryParseValue(GUI.TextField(new Rect(leftIndent + (90), (priorityLines * entryHeight), contentWidth - 90 - 38, entryHeight), textNumFields["targetWeightAccel"].possibleValue, 4)); + textNumFields["targetWeightAccel"].tryParseValue(GUI.TextField(InputFieldRect(priorityLines, priorityLabelWidth), textNumFields["targetWeightAccel"].possibleValue, 4, inputFieldStyle)); ActiveWeaponManager.targetWeightAccel = (float)textNumFields["targetWeightAccel"].currentValue; } - GUI.Label(new Rect(leftIndent + (contentWidth - 35), (priorityLines * entryHeight), 35, entryHeight), - ActiveWeaponManager.targetWeightAccel.ToString(), leftLabel); - priorityLines++; - GUI.Label(new Rect(leftIndent, (priorityLines * entryHeight), 85, entryHeight), Localizer.Format("#LOC_BDArmory_WMWindow_targetClosingTime"), leftLabel); //target proximity" + GUI.Label(LabelRect(++priorityLines, priorityLabelWidth), StringUtils.Localize("#LOC_BDArmory_WMWindow_targetClosingTime"), leftLabel); //target closing time" if (!NumFieldsEnabled) { - ActiveWeaponManager.targetWeightClosureTime = - GUI.HorizontalSlider( - new Rect(leftIndent + (150), (priorityLines * entryHeight), contentWidth - 150 - 38, entryHeight), - ActiveWeaponManager.targetWeightClosureTime, -10, 10); - ActiveWeaponManager.targetWeightClosureTime = Mathf.Round(ActiveWeaponManager.targetWeightClosureTime * 10) / 10; + ActiveWeaponManager.targetWeightClosureTime = BDAMath.RoundToUnit(GUI.HorizontalSlider(SliderRect(priorityLines, priorityLabelWidth), ActiveWeaponManager.targetWeightClosureTime, -10, 10), 0.1f); + GUI.Label(RightLabelRect(priorityLines), ActiveWeaponManager.targetWeightClosureTime.ToString(), leftLabel); } else { - textNumFields["targetWeightClosureTime"].tryParseValue(GUI.TextField(new Rect(leftIndent + (90), (priorityLines * entryHeight), contentWidth - 90 - 38, entryHeight), textNumFields["targetWeightClosureTime"].possibleValue, 4)); + textNumFields["targetWeightClosureTime"].tryParseValue(GUI.TextField(InputFieldRect(priorityLines, priorityLabelWidth), textNumFields["targetWeightClosureTime"].possibleValue, 4, inputFieldStyle)); ActiveWeaponManager.targetWeightClosureTime = (float)textNumFields["targetWeightClosureTime"].currentValue; } - GUI.Label(new Rect(leftIndent + (contentWidth - 35), (priorityLines * entryHeight), 35, entryHeight), - ActiveWeaponManager.targetWeightClosureTime.ToString(), leftLabel); - priorityLines++; - GUI.Label(new Rect(leftIndent, (priorityLines * entryHeight), 85, entryHeight), Localizer.Format("#LOC_BDArmory_WMWindow_targetgunNumber"), leftLabel); //target proximity" + GUI.Label(LabelRect(++priorityLines, priorityLabelWidth), StringUtils.Localize("#LOC_BDArmory_WMWindow_targetgunNumber"), leftLabel); //target weapon num." if (!NumFieldsEnabled) { - ActiveWeaponManager.targetWeightWeaponNumber = - GUI.HorizontalSlider( - new Rect(leftIndent + (150), (priorityLines * entryHeight), contentWidth - 150 - 38, entryHeight), - ActiveWeaponManager.targetWeightWeaponNumber, -10, 10); - ActiveWeaponManager.targetWeightWeaponNumber = Mathf.Round(ActiveWeaponManager.targetWeightWeaponNumber * 10) / 10; + ActiveWeaponManager.targetWeightWeaponNumber = BDAMath.RoundToUnit(GUI.HorizontalSlider(SliderRect(priorityLines, priorityLabelWidth), ActiveWeaponManager.targetWeightWeaponNumber, -10, 10), 0.1f); + GUI.Label(RightLabelRect(priorityLines), ActiveWeaponManager.targetWeightWeaponNumber.ToString(), leftLabel); } else { - textNumFields["targetWeightWeaponNumber"].tryParseValue(GUI.TextField(new Rect(leftIndent + (90), (priorityLines * entryHeight), contentWidth - 90 - 38, entryHeight), textNumFields["targetWeightWeaponNumber"].possibleValue, 4)); + textNumFields["targetWeightWeaponNumber"].tryParseValue(GUI.TextField(InputFieldRect(priorityLines, priorityLabelWidth), textNumFields["targetWeightWeaponNumber"].possibleValue, 4, inputFieldStyle)); ActiveWeaponManager.targetWeightWeaponNumber = (float)textNumFields["targetWeightWeaponNumber"].currentValue; } - GUI.Label(new Rect(leftIndent + (contentWidth - 35), (priorityLines * entryHeight), 35, entryHeight), - ActiveWeaponManager.targetWeightWeaponNumber.ToString(), leftLabel); - priorityLines++; - GUI.Label(new Rect(leftIndent, (priorityLines * entryHeight), 85, entryHeight), Localizer.Format("#LOC_BDArmory_WMWindow_targetMass"), leftLabel); //target proximity" + GUI.Label(LabelRect(++priorityLines, priorityLabelWidth), StringUtils.Localize("#LOC_BDArmory_WMWindow_targetMass"), leftLabel); //target mass" if (!NumFieldsEnabled) { - ActiveWeaponManager.targetWeightMass = - GUI.HorizontalSlider( - new Rect(leftIndent + (150), (priorityLines * entryHeight), contentWidth - 150 - 38, entryHeight), - ActiveWeaponManager.targetWeightMass, -10, 10); - ActiveWeaponManager.targetWeightMass = Mathf.Round(ActiveWeaponManager.targetWeightMass * 10) / 10; + ActiveWeaponManager.targetWeightMass = BDAMath.RoundToUnit(GUI.HorizontalSlider(SliderRect(priorityLines, priorityLabelWidth), ActiveWeaponManager.targetWeightMass, -10, 10), 0.1f); + GUI.Label(RightLabelRect(priorityLines), ActiveWeaponManager.targetWeightMass.ToString(), leftLabel); } else { - textNumFields["targetWeightMass"].tryParseValue(GUI.TextField(new Rect(leftIndent + (90), (priorityLines * entryHeight), contentWidth - 90 - 38, entryHeight), textNumFields["targetWeightMass"].possibleValue, 4)); + textNumFields["targetWeightMass"].tryParseValue(GUI.TextField(InputFieldRect(priorityLines, priorityLabelWidth), textNumFields["targetWeightMass"].possibleValue, 4, inputFieldStyle)); ActiveWeaponManager.targetWeightMass = (float)textNumFields["targetWeightMass"].currentValue; } - GUI.Label(new Rect(leftIndent + (contentWidth - 35), (priorityLines * entryHeight), 35, entryHeight), - ActiveWeaponManager.targetWeightMass.ToString(), leftLabel); - priorityLines++; - GUI.Label(new Rect(leftIndent, (priorityLines * entryHeight), 85, entryHeight), Localizer.Format("#LOC_BDArmory_WMWindow_targetAllies"), leftLabel); //target proximity" + GUI.Label(LabelRect(++priorityLines, priorityLabelWidth), StringUtils.Localize("#LOC_BDArmory_TargetPriority_TargetDmg"), leftLabel); //target Damage" if (!NumFieldsEnabled) { - ActiveWeaponManager.targetWeightFriendliesEngaging = - GUI.HorizontalSlider( - new Rect(leftIndent + (150), (priorityLines * entryHeight), contentWidth - 150 - 38, entryHeight), - ActiveWeaponManager.targetWeightFriendliesEngaging, -10, 10); - ActiveWeaponManager.targetWeightFriendliesEngaging = Mathf.Round(ActiveWeaponManager.targetWeightFriendliesEngaging * 10) / 10; + ActiveWeaponManager.targetWeightDamage = BDAMath.RoundToUnit(GUI.HorizontalSlider(SliderRect(priorityLines, priorityLabelWidth), ActiveWeaponManager.targetWeightDamage, -10, 10), 0.1f); + GUI.Label(RightLabelRect(priorityLines), ActiveWeaponManager.targetWeightDamage.ToString(), leftLabel); } else { - textNumFields["targetWeightFriendliesEngaging"].tryParseValue(GUI.TextField(new Rect(leftIndent + (90), (priorityLines * entryHeight), contentWidth - 90 - 38, entryHeight), textNumFields["targetWeightFriendliesEngaging"].possibleValue, 4)); + textNumFields["targetWeightDamage"].tryParseValue(GUI.TextField(InputFieldRect(priorityLines, priorityLabelWidth), textNumFields["targetWeightDamage"].possibleValue, 4, inputFieldStyle)); + ActiveWeaponManager.targetWeightDamage = (float)textNumFields["targetWeightDamage"].currentValue; + } + + GUI.Label(LabelRect(++priorityLines, priorityLabelWidth), StringUtils.Localize("#LOC_BDArmory_WMWindow_targetAllies"), leftLabel); //target mass" + if (!NumFieldsEnabled) + { + ActiveWeaponManager.targetWeightFriendliesEngaging = BDAMath.RoundToUnit(GUI.HorizontalSlider(SliderRect(priorityLines, priorityLabelWidth), ActiveWeaponManager.targetWeightFriendliesEngaging, -10, 10), 0.1f); + GUI.Label(RightLabelRect(priorityLines), ActiveWeaponManager.targetWeightFriendliesEngaging.ToString(), leftLabel); + } + else + { + textNumFields["targetWeightFriendliesEngaging"].tryParseValue(GUI.TextField(InputFieldRect(priorityLines, priorityLabelWidth), textNumFields["targetWeightFriendliesEngaging"].possibleValue, 4, inputFieldStyle)); ActiveWeaponManager.targetWeightFriendliesEngaging = (float)textNumFields["targetWeightFriendliesEngaging"].currentValue; } - GUI.Label(new Rect(leftIndent + (contentWidth - 35), (priorityLines * entryHeight), 35, entryHeight), - ActiveWeaponManager.targetWeightFriendliesEngaging.ToString(), leftLabel); - priorityLines++; - GUI.Label(new Rect(leftIndent, (priorityLines * entryHeight), 85, entryHeight), Localizer.Format("#LOC_BDArmory_WMWindow_targetThreat"), leftLabel); //target proximity" + GUI.Label(LabelRect(++priorityLines, priorityLabelWidth), StringUtils.Localize("#LOC_BDArmory_WMWindow_targetThreat"), leftLabel); //target proximity" if (!NumFieldsEnabled) { - ActiveWeaponManager.targetWeightThreat = - GUI.HorizontalSlider( - new Rect(leftIndent + (150), (priorityLines * entryHeight), contentWidth - 150 - 38, entryHeight), - ActiveWeaponManager.targetWeightThreat, -10, 10); - ActiveWeaponManager.targetWeightThreat = Mathf.Round(ActiveWeaponManager.targetWeightThreat * 10) / 10; + ActiveWeaponManager.targetWeightThreat = BDAMath.RoundToUnit(GUI.HorizontalSlider(SliderRect(priorityLines, priorityLabelWidth), ActiveWeaponManager.targetWeightThreat, -10, 10), 0.1f); + GUI.Label(RightLabelRect(priorityLines), ActiveWeaponManager.targetWeightThreat.ToString(), leftLabel); } else { - textNumFields["targetWeightThreat"].tryParseValue(GUI.TextField(new Rect(leftIndent + (90), (priorityLines * entryHeight), contentWidth - 90 - 38, entryHeight), textNumFields["targetWeightThreat"].possibleValue, 4)); + textNumFields["targetWeightThreat"].tryParseValue(GUI.TextField(InputFieldRect(priorityLines, priorityLabelWidth), textNumFields["targetWeightThreat"].possibleValue, 4, inputFieldStyle)); ActiveWeaponManager.targetWeightThreat = (float)textNumFields["targetWeightThreat"].currentValue; } - GUI.Label(new Rect(leftIndent + (contentWidth - 35), (priorityLines * entryHeight), 35, entryHeight), - ActiveWeaponManager.targetWeightThreat.ToString(), leftLabel); - priorityLines++; - GUI.Label(new Rect(leftIndent, (priorityLines * entryHeight), 85, entryHeight), Localizer.Format("#LOC_BDArmory_WMWindow_defendTeammate"), leftLabel); //defend teammate" + GUI.Label(LabelRect(++priorityLines, priorityLabelWidth), StringUtils.Localize("#LOC_BDArmory_WMWindow_defendTeammate"), leftLabel); //defend teammate" if (!NumFieldsEnabled) { - ActiveWeaponManager.targetWeightProtectTeammate = - GUI.HorizontalSlider( - new Rect(leftIndent + (150), (priorityLines * entryHeight), contentWidth - 150 - 38, entryHeight), - ActiveWeaponManager.targetWeightProtectTeammate, -10, 10); - ActiveWeaponManager.targetWeightProtectTeammate = Mathf.Round(ActiveWeaponManager.targetWeightProtectTeammate * 10) / 10; + ActiveWeaponManager.targetWeightProtectTeammate = BDAMath.RoundToUnit(GUI.HorizontalSlider(SliderRect(priorityLines, priorityLabelWidth), ActiveWeaponManager.targetWeightProtectTeammate, -10, 10), 0.1f); + GUI.Label(RightLabelRect(priorityLines), ActiveWeaponManager.targetWeightProtectTeammate.ToString(), leftLabel); } else { - textNumFields["targetWeightProtectTeammate"].tryParseValue(GUI.TextField(new Rect(leftIndent + (90), (priorityLines * entryHeight), contentWidth - 90 - 38, entryHeight), textNumFields["targetWeightProtectTeammate"].possibleValue, 4)); + textNumFields["targetWeightProtectTeammate"].tryParseValue(GUI.TextField(InputFieldRect(priorityLines, priorityLabelWidth), textNumFields["targetWeightProtectTeammate"].possibleValue, 4, inputFieldStyle)); ActiveWeaponManager.targetWeightProtectTeammate = (float)textNumFields["targetWeightProtectTeammate"].currentValue; } - GUI.Label(new Rect(leftIndent + (contentWidth - 35), (priorityLines * entryHeight), 35, entryHeight), - ActiveWeaponManager.targetWeightProtectTeammate.ToString(), leftLabel); - priorityLines++; - GUI.Label(new Rect(leftIndent, (priorityLines * entryHeight), 85, entryHeight), Localizer.Format("#LOC_BDArmory_WMWindow_defendVIP"), leftLabel); //target proximity" + GUI.Label(LabelRect(++priorityLines, priorityLabelWidth), StringUtils.Localize("#LOC_BDArmory_WMWindow_defendVIP"), leftLabel); //target proximity" if (!NumFieldsEnabled) { - ActiveWeaponManager.targetWeightProtectVIP = - GUI.HorizontalSlider( - new Rect(leftIndent + (150), (priorityLines * entryHeight), contentWidth - 150 - 38, entryHeight), - ActiveWeaponManager.targetWeightProtectVIP, -10, 10); - ActiveWeaponManager.targetWeightProtectVIP = Mathf.Round(ActiveWeaponManager.targetWeightProtectVIP * 10) / 10; + ActiveWeaponManager.targetWeightProtectVIP = BDAMath.RoundToUnit(GUI.HorizontalSlider(SliderRect(priorityLines, priorityLabelWidth), ActiveWeaponManager.targetWeightProtectVIP, -10, 10), 0.1f); + GUI.Label(RightLabelRect(priorityLines), ActiveWeaponManager.targetWeightProtectVIP.ToString(), leftLabel); } else { - textNumFields["targetWeightProtectVIP"].tryParseValue(GUI.TextField(new Rect(leftIndent + (90), (priorityLines * entryHeight), contentWidth - 90 - 38, entryHeight), textNumFields["targetWeightProtectVIP"].possibleValue, 4)); + textNumFields["targetWeightProtectVIP"].tryParseValue(GUI.TextField(InputFieldRect(priorityLines, priorityLabelWidth), textNumFields["targetWeightProtectVIP"].possibleValue, 4, inputFieldStyle)); ActiveWeaponManager.targetWeightProtectVIP = (float)textNumFields["targetWeightProtectVIP"].currentValue; } - GUI.Label(new Rect(leftIndent + (contentWidth - 35), (priorityLines * entryHeight), 35, entryHeight), - ActiveWeaponManager.targetWeightProtectVIP.ToString(), leftLabel); - priorityLines++; - GUI.Label(new Rect(leftIndent, (priorityLines * entryHeight), 85, entryHeight), Localizer.Format("#LOC_BDArmory_WMWindow_targetVIP"), leftLabel); //target proximity" + GUI.Label(LabelRect(++priorityLines, priorityLabelWidth), StringUtils.Localize("#LOC_BDArmory_WMWindow_targetVIP"), leftLabel); //target proximity" if (!NumFieldsEnabled) { - ActiveWeaponManager.targetWeightAttackVIP = - GUI.HorizontalSlider( - new Rect(leftIndent + (150), (priorityLines * entryHeight), contentWidth - 150 - 38, entryHeight), - ActiveWeaponManager.targetWeightAttackVIP, -10, 10); - ActiveWeaponManager.targetWeightAttackVIP = Mathf.Round(ActiveWeaponManager.targetWeightAttackVIP * 10) / 10; + ActiveWeaponManager.targetWeightAttackVIP = BDAMath.RoundToUnit(GUI.HorizontalSlider(SliderRect(priorityLines, priorityLabelWidth), ActiveWeaponManager.targetWeightAttackVIP, -10, 10), 0.1f); + GUI.Label(RightLabelRect(priorityLines), ActiveWeaponManager.targetWeightAttackVIP.ToString(), leftLabel); } else { - textNumFields["targetWeightAttackVIP"].tryParseValue(GUI.TextField(new Rect(leftIndent + (90), (priorityLines * entryHeight), contentWidth - 90 - 38, entryHeight), textNumFields["targetWeightAttackVIP"].possibleValue, 4)); + textNumFields["targetWeightAttackVIP"].tryParseValue(GUI.TextField(InputFieldRect(priorityLines, priorityLabelWidth), textNumFields["targetWeightAttackVIP"].possibleValue, 4, inputFieldStyle)); ActiveWeaponManager.targetWeightAttackVIP = (float)textNumFields["targetWeightAttackVIP"].currentValue; } - GUI.Label(new Rect(leftIndent + (contentWidth - 35), (priorityLines * entryHeight), 35, entryHeight), - ActiveWeaponManager.targetWeightAttackVIP.ToString(), leftLabel); - priorityLines++; - priorityLines += 0.1f; + priorityLines += 1.1f; GUI.EndGroup(); } priorityheight = Mathf.Lerp(priorityheight, priorityLines, 0.15f); @@ -1807,8 +1815,8 @@ void WindowBDAToolbar(int windowID) { numberOfModules++; bool isEnabled = ActiveWeaponManager.rwr.displayRWR; - string label = Localizer.Format("#LOC_BDArmory_WMWindow_RadarWarning");//"Radar Warning Receiver" - Rect rwrRect = new Rect(leftIndent, +(moduleLines * entryHeight), contentWidth, entryHeight); + string label = StringUtils.Localize("#LOC_BDArmory_WMWindow_RadarWarning");//"Radar Warning Receiver" + Rect rwrRect = new Rect(leftIndent, +(moduleLines * entryHeight), columnWidth - 2 * leftIndent, entryHeight); if (GUI.Button(rwrRect, label, isEnabled ? centerLabelOrange : centerLabel)) { if (isEnabled) @@ -1826,71 +1834,98 @@ void WindowBDAToolbar(int windowID) } //TGP - List.Enumerator mtc = ActiveWeaponManager.targetingPods.GetEnumerator(); - while (mtc.MoveNext()) - { - if (mtc.Current == null) continue; - numberOfModules++; - bool isEnabled = (mtc.Current.cameraEnabled); - bool isActive = (mtc.Current == ModuleTargetingCamera.activeCam); - GUIStyle moduleStyle = isEnabled ? centerLabelOrange : centerLabel; // = mtc - string label = mtc.Current.part.partInfo.title; - if (isActive) - { - moduleStyle = centerLabelRed; - label = "[" + label + "]"; - } - if (GUI.Button(new Rect(leftIndent, +(moduleLines * entryHeight), contentWidth, entryHeight), - label, moduleStyle)) + using (List.Enumerator mtc = ActiveWeaponManager.targetingPods.GetEnumerator()) + while (mtc.MoveNext()) { + if (mtc.Current == null) continue; + numberOfModules++; + bool isEnabled = (mtc.Current.cameraEnabled); + bool isActive = (mtc.Current == ModuleTargetingCamera.activeCam); + GUIStyle moduleStyle = isEnabled ? centerLabelOrange : centerLabel; // = mtc + string label = mtc.Current.part.partInfo.title; if (isActive) { - mtc.Current.ToggleCamera(); + moduleStyle = centerLabelRed; + label = $"[{label}]"; } - else + if (GUI.Button(new Rect(leftIndent, +(moduleLines * entryHeight), columnWidth - 2 * leftIndent, entryHeight), + label, moduleStyle)) { - mtc.Current.EnableCamera(); + if (isActive) + { + mtc.Current.ToggleCamera(); + } + else + { + mtc.Current.EnableCamera(); + } } + moduleLines++; } - moduleLines++; - } - mtc.Dispose(); //RADAR - List.Enumerator mr = ActiveWeaponManager.radars.GetEnumerator(); - while (mr.MoveNext()) - { - if (mr.Current == null) continue; - numberOfModules++; - GUIStyle moduleStyle = mr.Current.radarEnabled ? centerLabelBlue : centerLabel; - string label = mr.Current.radarName; - if (GUI.Button(new Rect(leftIndent, +(moduleLines * entryHeight), contentWidth, entryHeight), - label, moduleStyle)) + using (List.Enumerator mr = ActiveWeaponManager.radars.GetEnumerator()) + while (mr.MoveNext()) { - mr.Current.Toggle(); + if (mr.Current == null) continue; + numberOfModules++; + GUIStyle moduleStyle = mr.Current.radarEnabled ? centerLabelBlue : centerLabel; + string label = mr.Current.radarName; + if (GUI.Button(new Rect(leftIndent, +(moduleLines * entryHeight), columnWidth - 2 * leftIndent, entryHeight), + label, moduleStyle)) + { + mr.Current.Toggle(); + } + moduleLines++; + } + using (List.Enumerator mr = ActiveWeaponManager.irsts.GetEnumerator()) + while (mr.MoveNext()) + { + if (mr.Current == null) continue; + numberOfModules++; + GUIStyle moduleStyle = mr.Current.irstEnabled ? centerLabelBlue : centerLabel; + string label = mr.Current.IRSTName; + if (GUI.Button(new Rect(leftIndent, +(moduleLines * entryHeight), columnWidth - 2 * leftIndent, entryHeight), + label, moduleStyle)) + { + mr.Current.Toggle(); + } + moduleLines++; } - moduleLines++; - } - mr.Dispose(); - //JAMMERS - List.Enumerator jammer = ActiveWeaponManager.jammers.GetEnumerator(); - while (jammer.MoveNext()) - { - if (jammer.Current == null) continue; - if (jammer.Current.alwaysOn) continue; + using (List.Enumerator jammer = ActiveWeaponManager.jammers.GetEnumerator()) + while (jammer.MoveNext()) + { + if (jammer.Current == null) continue; + if (jammer.Current.alwaysOn) continue; - numberOfModules++; - GUIStyle moduleStyle = jammer.Current.jammerEnabled ? centerLabelBlue : centerLabel; - string label = jammer.Current.part.partInfo.title; - if (GUI.Button(new Rect(leftIndent, +(moduleLines * entryHeight), contentWidth, entryHeight), - label, moduleStyle)) + numberOfModules++; + GUIStyle moduleStyle = jammer.Current.jammerEnabled ? centerLabelBlue : centerLabel; + string label = jammer.Current.part.partInfo.title; + if (GUI.Button(new Rect(leftIndent, +(moduleLines * entryHeight), columnWidth - 2 * leftIndent, entryHeight), + label, moduleStyle)) + { + jammer.Current.Toggle(); + } + moduleLines++; + } + //CLOAKS + using (List.Enumerator cloak = ActiveWeaponManager.cloaks.GetEnumerator()) + while (cloak.MoveNext()) { - jammer.Current.Toggle(); + if (cloak.Current == null) continue; + if (cloak.Current.alwaysOn) continue; + + numberOfModules++; + GUIStyle moduleStyle = cloak.Current.cloakEnabled ? centerLabelBlue : centerLabel; + string label = cloak.Current.part.partInfo.title; + if (GUI.Button(new Rect(leftIndent, +(moduleLines * entryHeight), columnWidth - 2 * leftIndent, entryHeight), + label, moduleStyle)) + { + cloak.Current.Toggle(); + } + moduleLines++; } - moduleLines++; - } - jammer.Dispose(); //Other modules using (var module = ActiveWeaponManager.wmModules.GetEnumerator()) @@ -1901,7 +1936,7 @@ void WindowBDAToolbar(int windowID) numberOfModules++; GUIStyle moduleStyle = module.Current.Enabled ? centerLabelBlue : centerLabel; string label = module.Current.Name; - if (GUI.Button(new Rect(leftIndent, +(moduleLines * entryHeight), contentWidth, entryHeight), + if (GUI.Button(new Rect(leftIndent, +(moduleLines * entryHeight), columnWidth - 2 * leftIndent, entryHeight), label, moduleStyle)) { module.Current.Toggle(); @@ -1912,8 +1947,8 @@ void WindowBDAToolbar(int windowID) //GPS coordinator GUIStyle gpsModuleStyle = showWindowGPS ? centerLabelBlue : centerLabel; numberOfModules++; - if (GUI.Button(new Rect(leftIndent, +(moduleLines * entryHeight), contentWidth, entryHeight), - Localizer.Format("#LOC_BDArmory_WMWindow_GPSCoordinator"), gpsModuleStyle))//"GPS Coordinator" + if (GUI.Button(new Rect(leftIndent, +(moduleLines * entryHeight), columnWidth - 2 * leftIndent, entryHeight), + StringUtils.Localize("#LOC_BDArmory_WMWindow_GPSCoordinator"), gpsModuleStyle))//"GPS Coordinator" { showWindowGPS = !showWindowGPS; } @@ -1926,8 +1961,8 @@ void WindowBDAToolbar(int windowID) ? centerLabelBlue : centerLabel; numberOfModules++; - if (GUI.Button(new Rect(leftIndent, +(moduleLines * entryHeight), contentWidth, entryHeight), - Localizer.Format("#LOC_BDArmory_WMWindow_WingCommand"), wingComStyle))//"Wing Command" + if (GUI.Button(new Rect(leftIndent, +(moduleLines * entryHeight), columnWidth - 2 * leftIndent, entryHeight), + StringUtils.Localize("#LOC_BDArmory_WMWindow_WingCommand"), wingComStyle))//"Wing Command" { ActiveWeaponManager.wingCommander.ToggleGUI(); } @@ -1956,47 +1991,50 @@ void WindowBDAToolbar(int windowID) { windowColumns = 2; - GUI.Label(new Rect(leftIndent + columnWidth, contentTop, columnWidth - (leftIndent), entryHeight), Localizer.Format("#LOC_BDArmory_AIWindow_infoLink"), kspTitleLabel);//"infolink" + GUI.Label(new Rect(leftIndent + columnWidth, contentTop, columnWidth - (leftIndent), entryHeight), StringUtils.Localize("#LOC_BDArmory_AIWindow_infoLink"), kspTitleLabel);//"infolink" GUILayout.BeginArea(new Rect(leftIndent + columnWidth, contentTop + (entryHeight * 1.5f), columnWidth - (leftIndent), toolWindowHeight - (entryHeight * 1.5f) - (2 * contentTop))); using (var scrollViewScope = new GUILayout.ScrollViewScope(scrollInfoVector, GUILayout.Width(columnWidth - (leftIndent)), GUILayout.Height(toolWindowHeight - (entryHeight * 1.5f) - (2 * contentTop)))) { scrollInfoVector = scrollViewScope.scrollPosition; if (showWeaponList) { - GUILayout.Label(Localizer.Format("#LOC_BDArmory_WMWindow_ListWeapons"), leftLabelBold, GUILayout.Width(columnWidth - (leftIndent * 4) - 20)); //Weapons - GUILayout.Label(Localizer.Format("#LOC_BDArmory_WMWindow_Weapons_Desc"), infoLinkStyle, GUILayout.Width(columnWidth - (leftIndent * 4) - 20)); //weapons desc - GUILayout.Label(Localizer.Format("#LOC_BDArmory_WMWindow_Ripple_Salvo_Desc"), infoLinkStyle, GUILayout.Width(columnWidth - (leftIndent * 4) - 20)); //ripple/salvo desc + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_WMWindow_ListWeapons"), leftLabelBold, GUILayout.Width(columnWidth - (leftIndent * 4) - 20)); //Weapons + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_WMWindow_Weapons_Desc"), infoLinkStyle, GUILayout.Width(columnWidth - (leftIndent * 4) - 20)); //weapons desc + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_WMWindow_Ripple_Salvo_Desc"), infoLinkStyle, GUILayout.Width(columnWidth - (leftIndent * 4) - 20)); //ripple/salvo desc } if (showGuardMenu) { - GUILayout.Label(Localizer.Format("#LOC_BDArmory_WMWindow_GuardMenu"), leftLabelBold, GUILayout.Width(columnWidth - (leftIndent * 4) - 20)); //Guard Mode - GUILayout.Label(Localizer.Format("#LOC_BDArmory_WMWindow_GuardTab_Desc"), infoLinkStyle, GUILayout.Width(columnWidth - (leftIndent * 4) - 20)); //Guard desc - GUILayout.Label(Localizer.Format("#LOC_BDArmory_WMWindow_FiringInterval_Desc"), infoLinkStyle, GUILayout.Width(columnWidth - (leftIndent * 4) - 20)); //firing inverval desc - GUILayout.Label(Localizer.Format("#LOC_BDArmory_WMWindow_BurstLength_desc"), infoLinkStyle, GUILayout.Width(columnWidth - (leftIndent * 4) - 20)); //burst length desc + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_WMWindow_GuardMenu"), leftLabelBold, GUILayout.Width(columnWidth - (leftIndent * 4) - 20)); //Guard Mode + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_WMWindow_GuardTab_Desc"), infoLinkStyle, GUILayout.Width(columnWidth - (leftIndent * 4) - 20)); //Guard desc + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_WMWindow_FiringInterval_Desc"), infoLinkStyle, GUILayout.Width(columnWidth - (leftIndent * 4) - 20)); //firing inverval desc + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_WMWindow_BurstLength_desc"), infoLinkStyle, GUILayout.Width(columnWidth - (leftIndent * 4) - 20)); //burst length desc GUILayout.Label(FiringAngleImage); - GUILayout.Label(Localizer.Format("#LOC_BDArmory_WMWindow_FiringTolerance_desc"), infoLinkStyle, GUILayout.Width(columnWidth - (leftIndent * 4) - 20)); //firing angle desc - GUILayout.Label(Localizer.Format("#LOC_BDArmory_WMWindow_FieldofView_desc"), infoLinkStyle, GUILayout.Width(columnWidth - (leftIndent * 4) - 20)); //FoV desc - GUILayout.Label(Localizer.Format("#LOC_BDArmory_WMWindow_VisualRange_desc"), infoLinkStyle, GUILayout.Width(columnWidth - (leftIndent * 4) - 20)); //guard range desc - GUILayout.Label(Localizer.Format("#LOC_BDArmory_WMWindow_GunsRange_desc"), infoLinkStyle, GUILayout.Width(columnWidth - (leftIndent * 4) - 20)); //weapon range desc - GUILayout.Label(Localizer.Format("#LOC_BDArmory_WMWindow_MultiTargetNum_desc"), infoLinkStyle, GUILayout.Width(columnWidth - (leftIndent * 4) - 20)); //multiturrets desc - GUILayout.Label(Localizer.Format("#LOC_BDArmory_WMWindow_MissilesTgt_desc"), infoLinkStyle, GUILayout.Width(columnWidth - (leftIndent * 4) - 20)); //multimissiles desc - GUILayout.Label(Localizer.Format("#LOC_BDArmory_WMWindow_TargetType_desc"), infoLinkStyle, GUILayout.Width(columnWidth - (leftIndent * 4) - 20)); //subsection targeting desc - GUILayout.Label(Localizer.Format("#LOC_BDArmory_WMWindow_EngageType_desc"), infoLinkStyle, GUILayout.Width(columnWidth - (leftIndent * 4) - 20)); //engagement toggles desc + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_WMWindow_FiringTolerance_desc"), infoLinkStyle, GUILayout.Width(columnWidth - (leftIndent * 4) - 20)); //firing angle desc + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_WMWindow_FieldofView_desc"), infoLinkStyle, GUILayout.Width(columnWidth - (leftIndent * 4) - 20)); //FoV desc + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_WMWindow_VisualRange_desc"), infoLinkStyle, GUILayout.Width(columnWidth - (leftIndent * 4) - 20)); //guard range desc + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_WMWindow_GunsRange_desc"), infoLinkStyle, GUILayout.Width(columnWidth - (leftIndent * 4) - 20)); //weapon range desc + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_WMWindow_MultiTargetNum_desc"), infoLinkStyle, GUILayout.Width(columnWidth - (leftIndent * 4) - 20)); //multiturrets desc + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_WMWindow_MultiMissileTgtNum_desc"), infoLinkStyle, GUILayout.Width(columnWidth - (leftIndent * 4) - 20)); //multiturrets desc + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_WMWindow_MissilesTgt_desc"), infoLinkStyle, GUILayout.Width(columnWidth - (leftIndent * 4) - 20)); //multimissiles desc + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_WMWindow_TargetType_desc"), infoLinkStyle, GUILayout.Width(columnWidth - (leftIndent * 4) - 20)); //subsection targeting desc + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_WMWindow_EngageType_desc"), infoLinkStyle, GUILayout.Width(columnWidth - (leftIndent * 4) - 20)); //engagement toggles desc } if (showPriorities) { - GUILayout.Label(Localizer.Format("#LOC_BDArmory_WMWindow_Prioritues_Desc"), leftLabelBold, GUILayout.Width(columnWidth - (leftIndent * 4) - 20)); //Tgt Priorities - GUILayout.Label(Localizer.Format("#LOC_BDArmory_WMWindow_targetBias_desc"), infoLinkStyle, GUILayout.Width(columnWidth - (leftIndent * 4) - 20)); //Tgt Bias - GUILayout.Label(Localizer.Format("#LOC_BDArmory_WMWindow_targetProximity_desc"), infoLinkStyle, GUILayout.Width(columnWidth - (leftIndent * 4) - 20)); //Tgt dist - GUILayout.Label(Localizer.Format("#LOC_BDArmory_WMWindow_targetAngletoTarget_desc"), infoLinkStyle, GUILayout.Width(columnWidth - (leftIndent * 4) - 20)); //Tgt angle - GUILayout.Label(Localizer.Format("#LOC_BDArmory_WMWindow_targetAngleDist_desc"), infoLinkStyle, GUILayout.Width(columnWidth - (leftIndent * 4) - 20)); //Tgt angle/dist - GUILayout.Label(Localizer.Format("#LOC_BDArmory_WMWindow_targetAccel_desc"), infoLinkStyle, GUILayout.Width(columnWidth - (leftIndent * 4) - 20)); //Tgt accel - GUILayout.Label(Localizer.Format("#LOC_BDArmory_WMWindow_targetClosingTime_desc"), infoLinkStyle, GUILayout.Width(columnWidth - (leftIndent * 4) - 20)); //Tgt closing time - GUILayout.Label(Localizer.Format("#LOC_BDArmory_WMWindow_targetgunNumber_desc"), infoLinkStyle, GUILayout.Width(columnWidth - (leftIndent * 4) - 20)); //Tgt weapons num - GUILayout.Label(Localizer.Format("#LOC_BDArmory_WMWindow_targetMass_desc"), infoLinkStyle, GUILayout.Width(columnWidth - (leftIndent * 4) - 20)); //Tgt mass - GUILayout.Label(Localizer.Format("#LOC_BDArmory_WMWindow_targetAllies_desc"), infoLinkStyle, GUILayout.Width(columnWidth - (leftIndent * 4) - 20)); //Tgt allies attacking - GUILayout.Label(Localizer.Format("#LOC_BDArmory_WMWindow_targetThreat_desc"), infoLinkStyle, GUILayout.Width(columnWidth - (leftIndent * 4) - 20)); //Tgt threat - GUILayout.Label(Localizer.Format("#LOC_BDArmory_WMWindow_targetVIP_desc"), infoLinkStyle, GUILayout.Width(columnWidth - (leftIndent * 4) - 20)); //Tgt VIP + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_WMWindow_Prioritues_Desc"), leftLabelBold, GUILayout.Width(columnWidth - (leftIndent * 4) - 20)); //Tgt Priorities + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_WMWindow_targetBias_desc"), infoLinkStyle, GUILayout.Width(columnWidth - (leftIndent * 4) - 20)); //Tgt Bias + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_WMWindow_targetPreference_desc"), infoLinkStyle, GUILayout.Width(columnWidth - (leftIndent * 4) - 20)); //Tgt engagement Pref + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_WMWindow_targetProximity_desc"), infoLinkStyle, GUILayout.Width(columnWidth - (leftIndent * 4) - 20)); //Tgt dist + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_WMWindow_targetAngletoTarget_desc"), infoLinkStyle, GUILayout.Width(columnWidth - (leftIndent * 4) - 20)); //Tgt angle + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_WMWindow_targetAngleDist_desc"), infoLinkStyle, GUILayout.Width(columnWidth - (leftIndent * 4) - 20)); //Tgt angle/dist + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_WMWindow_targetAccel_desc"), infoLinkStyle, GUILayout.Width(columnWidth - (leftIndent * 4) - 20)); //Tgt accel + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_WMWindow_targetClosingTime_desc"), infoLinkStyle, GUILayout.Width(columnWidth - (leftIndent * 4) - 20)); //Tgt closing time + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_WMWindow_targetgunNumber_desc"), infoLinkStyle, GUILayout.Width(columnWidth - (leftIndent * 4) - 20)); //Tgt weapons num + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_WMWindow_targetMass_desc"), infoLinkStyle, GUILayout.Width(columnWidth - (leftIndent * 4) - 20)); //Tgt mass + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_WMWindow_targetDmg_desc"), infoLinkStyle, GUILayout.Width(columnWidth - (leftIndent * 4) - 20)); //Tgt Damage + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_WMWindow_targetAllies_desc"), infoLinkStyle, GUILayout.Width(columnWidth - (leftIndent * 4) - 20)); //Tgt allies attacking + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_WMWindow_targetThreat_desc"), infoLinkStyle, GUILayout.Width(columnWidth - (leftIndent * 4) - 20)); //Tgt threat + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_WMWindow_targetVIP_desc"), infoLinkStyle, GUILayout.Width(columnWidth - (leftIndent * 4) - 20)); //Tgt VIP } } @@ -2005,10 +2043,21 @@ void WindowBDAToolbar(int windowID) } else { - GUI.Label(new Rect(leftIndent, contentTop + (line * entryHeight), contentWidth, entryHeight), - Localizer.Format("#LOC_BDArmory_WMWindow_NoWeaponManager"), BDGuiSkin.box);// "No Weapon Manager found." + GUI.Label(new Rect(leftIndent, contentTop + (line * entryHeight), columnWidth - 2 * leftIndent, entryHeight), + StringUtils.Localize("#LOC_BDArmory_WMWindow_NoWeaponManager"), BDGuiSkin.box);// "No Weapon Manager found." line++; } +#if DEBUG + if (GUI.Button(new Rect(leftIndent, contentTop + (line++ * entryHeight), contentWidth, entryHeight * 1.25f), "Double click to QUIT", Time.realtimeSinceStartup - quitTimer > 1 ? BDGuiSkin.button : BDGuiSkin.box)) // Big QUIT button for debug mode. Double click within 1s to quit. + { + if (Time.realtimeSinceStartup - quitTimer < 1) + { + SaveConfig(); + TournamentAutoResume.AutoQuit(0); + } + quitTimer = Time.realtimeSinceStartup; + } +#endif toolWindowWidth = Mathf.Lerp(toolWindowWidth, columnWidth * windowColumns, 0.15f); toolWindowHeight = Mathf.Lerp(toolWindowHeight, contentTop + (line * entryHeight) + 5, 1); var previousWindowHeight = WindowRectToolbar.height; @@ -2017,8 +2066,11 @@ void WindowBDAToolbar(int windowID) numberOfButtons = buttonNumber + 1; if (BDArmorySettings.STRICT_WINDOW_BOUNDARIES && toolWindowHeight < previousWindowHeight && Mathf.Round(WindowRectToolbar.y + previousWindowHeight) == Screen.height) // Window shrunk while being at edge of screen. WindowRectToolbar.y = Screen.height - WindowRectToolbar.height; - BDGUIUtils.RepositionWindow(ref WindowRectToolbar); + GUIUtils.RepositionWindow(ref WindowRectToolbar); } +#if DEBUG + float quitTimer = 0; +#endif bool validGPSName = true; @@ -2030,7 +2082,7 @@ public void WindowGPS() Rect listRect = new Rect(gpsBorder, gpsBorder, WindowRectGps.width - (2 * gpsBorder), WindowRectGps.height - (2 * gpsBorder)); GUI.BeginGroup(listRect); - string targetLabel = Localizer.Format("#LOC_BDArmory_WMWindow_GPSTarget") + ": " + ActiveWeaponManager.designatedGPSInfo.name;//GPS Target + string targetLabel = $"{StringUtils.Localize("#LOC_BDArmory_WMWindow_GPSTarget")}: {ActiveWeaponManager.designatedGPSInfo.name}";//GPS Target GUI.Label(new Rect(0, 0, listRect.width, gpsEntryHeight), targetLabel, kspTitleLabel); // Expand/Collapse Target Toggle button @@ -2041,7 +2093,7 @@ public void WindowGPS() if (ActiveWeaponManager.designatedGPSCoords != Vector3d.zero) { GUI.Label(new Rect(0, gpsEntryCount * gpsEntryHeight, listRect.width - gpsEntryHeight, gpsEntryHeight), - Utils.FormattedGeoPos(ActiveWeaponManager.designatedGPSCoords, true), BDGuiSkin.box); + BodyUtils.FormattedGeoPos(ActiveWeaponManager.designatedGPSCoords, true), BDGuiSkin.box); if ( GUI.Button( new Rect(listRect.width - gpsEntryHeight, gpsEntryCount * gpsEntryHeight, gpsEntryHeight, @@ -2053,7 +2105,7 @@ public void WindowGPS() else { GUI.Label(new Rect(0, gpsEntryCount * gpsEntryHeight, listRect.width - gpsEntryHeight, gpsEntryHeight), - Localizer.Format("#LOC_BDArmory_WMWindow_NoTarget"), BDGuiSkin.box);//"No Target" + StringUtils.Localize("#LOC_BDArmory_WMWindow_NoTarget"), BDGuiSkin.box);//"No Target" } gpsEntryCount += 1.35f; @@ -2062,77 +2114,77 @@ public void WindowGPS() BDTeam myTeam = ActiveWeaponManager.Team; if (showTargets) { - List.Enumerator coordinate = BDATargetManager.GPSTargetList(myTeam).GetEnumerator(); - while (coordinate.MoveNext()) - { - Color origWColor = GUI.color; - if (coordinate.Current.EqualsTarget(ActiveWeaponManager.designatedGPSInfo)) - { - GUI.color = XKCDColors.LightOrange; - } - - string label = Utils.FormattedGeoPosShort(coordinate.Current.gpsCoordinates, false); - float nameWidth = 100; - if (editingGPSName && index == editingGPSNameIndex) + using (var coordinate = BDATargetManager.GPSTargetList(myTeam).GetEnumerator()) + while (coordinate.MoveNext()) { - if (validGPSName && Event.current.type == EventType.KeyDown && - Event.current.keyCode == KeyCode.Return) + Color origWColor = GUI.color; + if (coordinate.Current.EqualsTarget(ActiveWeaponManager.designatedGPSInfo)) { - editingGPSName = false; - hasEnteredGPSName = true; + GUI.color = XKCDColors.LightOrange; } - else + + string label = BodyUtils.FormattedGeoPosShort(coordinate.Current.gpsCoordinates, false); + float nameWidth = 100; + if (editingGPSName && index == editingGPSNameIndex) { - Color origColor = GUI.color; - if (newGPSName.Contains(";") || newGPSName.Contains(":") || newGPSName.Contains(",")) + if (validGPSName && Event.current.type == EventType.KeyDown && + Event.current.keyCode == KeyCode.Return) { - validGPSName = false; - GUI.color = Color.red; + editingGPSName = false; + hasEnteredGPSName = true; } else { - validGPSName = true; - } + Color origColor = GUI.color; + if (newGPSName.Contains(";") || newGPSName.Contains(":") || newGPSName.Contains(",")) + { + validGPSName = false; + GUI.color = Color.red; + } + else + { + validGPSName = true; + } - newGPSName = GUI.TextField( - new Rect(0, gpsEntryCount * gpsEntryHeight, nameWidth, gpsEntryHeight), newGPSName, 12); - GUI.color = origColor; + newGPSName = GUI.TextField( + new Rect(0, gpsEntryCount * gpsEntryHeight, nameWidth, gpsEntryHeight), newGPSName, 12, inputFieldStyle); + GUI.color = origColor; + } } - } - else - { - if (GUI.Button(new Rect(0, gpsEntryCount * gpsEntryHeight, nameWidth, gpsEntryHeight), - coordinate.Current.name, - BDGuiSkin.button)) + else { - editingGPSName = true; - editingGPSNameIndex = index; - newGPSName = coordinate.Current.name; + if (GUI.Button(new Rect(0, gpsEntryCount * gpsEntryHeight, nameWidth, gpsEntryHeight), + coordinate.Current.name, + BDGuiSkin.button)) + { + editingGPSName = true; + editingGPSNameIndex = index; + newGPSName = coordinate.Current.name; + } } - } - if ( - GUI.Button( - new Rect(nameWidth, gpsEntryCount * gpsEntryHeight, listRect.width - gpsEntryHeight - nameWidth, - gpsEntryHeight), label, BDGuiSkin.button)) - { - ActiveWeaponManager.designatedGPSInfo = coordinate.Current; - editingGPSName = false; - } + if ( + GUI.Button( + new Rect(nameWidth, gpsEntryCount * gpsEntryHeight, listRect.width - gpsEntryHeight - nameWidth, + gpsEntryHeight), label, BDGuiSkin.button)) + { + ActiveWeaponManager.designatedGPSInfo = coordinate.Current; + ActiveWeaponManager.designatedGPSCoordsIndex = index; + editingGPSName = false; + } - if ( - GUI.Button( - new Rect(listRect.width - gpsEntryHeight, gpsEntryCount * gpsEntryHeight, gpsEntryHeight, - gpsEntryHeight), "X", BDGuiSkin.button)) - { - indexToRemove = index; - } + if ( + GUI.Button( + new Rect(listRect.width - gpsEntryHeight, gpsEntryCount * gpsEntryHeight, gpsEntryHeight, + gpsEntryHeight), "X", BDGuiSkin.button)) + { + indexToRemove = index; + } - gpsEntryCount++; - index++; - GUI.color = origWColor; - } - coordinate.Dispose(); + gpsEntryCount++; + index++; + GUI.color = origWColor; + } } if (hasEnteredGPSName && editingGPSNameIndex < BDATargetManager.GPSTargetList(myTeam).Count) @@ -2196,14 +2248,14 @@ Rect SRightButtonRect(float line) return new Rect(settingsWidth / 2 + settingsMargin / 4, line * settingsLineHeight, (settingsWidth - 2 * settingsMargin) / 2 - settingsMargin / 4, settingsLineHeight); } - Rect SLineThirdRect(float line, int pos) + Rect SLineThirdRect(float line, int pos, int span = 1) { - return new Rect(settingsMargin + pos * (settingsWidth - 2f * settingsMargin) / 3f, line * settingsLineHeight, (settingsWidth - 2f * settingsMargin) / 3f, settingsLineHeight); + return new Rect(settingsMargin + pos * (settingsWidth - 2f * settingsMargin) / 3f, line * settingsLineHeight, span * (settingsWidth - 2f * settingsMargin) / 3f, settingsLineHeight); } - Rect SQuarterRect(float line, int pos) + Rect SQuarterRect(float line, int pos, int span = 1) { - return new Rect(settingsMargin + (pos % 4) * (settingsWidth - 2f * settingsMargin) / 4f, (line + (int)(pos / 4)) * settingsLineHeight, (settingsWidth - 2.5f * settingsMargin) / 4f, settingsLineHeight); + return new Rect(settingsMargin + (pos % 4) * (settingsWidth - 2f * settingsMargin) / 4f, (line + (int)(pos / 4)) * settingsLineHeight, span * (settingsWidth - 2f * settingsMargin) / 4f, settingsLineHeight); } Rect SEighthRect(float line, int pos) @@ -2244,6 +2296,9 @@ List SRight3Rects(float line) public List selectedMutators; float mutatorHeight = 25; bool editKeys; +#if DEBUG + // int debug_numRaycasts = 4; +#endif void SetupSettingsSize() { @@ -2259,7 +2314,7 @@ void SetupSettingsSize() void WindowSettings(int windowID) { float line = 0.25f; // Top internal margin. - GUI.Box(new Rect(0, 0, settingsWidth, settingsHeight), Localizer.Format("#LOC_BDArmory_Settings_Title"));//"BDArmory Settings" + GUI.Box(new Rect(0, 0, settingsWidth, settingsHeight), StringUtils.Localize("#LOC_BDArmory_Settings_Title"));//"BDArmory Settings" if (GUI.Button(new Rect(settingsWidth - 18, 2, 16, 16), "X")) { windowSettingsEnabled = false; @@ -2271,83 +2326,478 @@ void WindowSettings(int windowID) return; } - GameSettings.ADVANCED_TWEAKABLES = GUI.Toggle(GameSettings.ADVANCED_TWEAKABLES ? SLeftRect(++line) : SLineRect(++line), GameSettings.ADVANCED_TWEAKABLES, Localizer.Format("#autoLOC_900906") + (GameSettings.ADVANCED_TWEAKABLES ? "" : " <— Access many more AI tuning options")); // Advanced tweakables - BDArmorySettings.ADVANDED_USER_SETTINGS = GUI.Toggle(GameSettings.ADVANCED_TWEAKABLES ? SRightRect(line) : SLineRect(++line), BDArmorySettings.ADVANDED_USER_SETTINGS, Localizer.Format("#LOC_BDArmory_Settings_AdvancedUserSettings"));// Advanced User Settings + GameSettings.ADVANCED_TWEAKABLES = GUI.Toggle(GameSettings.ADVANCED_TWEAKABLES ? SLeftRect(++line) : SLineRect(++line), GameSettings.ADVANCED_TWEAKABLES, StringUtils.Localize("#autoLOC_900906") + (GameSettings.ADVANCED_TWEAKABLES ? "" : " <— Access many more AI tuning options")); // Advanced tweakables + BDArmorySettings.ADVANCED_USER_SETTINGS = GUI.Toggle(GameSettings.ADVANCED_TWEAKABLES ? SRightRect(line) : SLineRect(++line), BDArmorySettings.ADVANCED_USER_SETTINGS, StringUtils.Localize("#LOC_BDArmory_Settings_AdvancedUserSettings"));// Advanced User Settings - if (GUI.Button(SLineRect(++line), (BDArmorySettings.GRAPHICS_UI_SECTION_TOGGLE ? "Hide " : "Show ") + Localizer.Format("#LOC_BDArmory_Settings_GraphicsSettingsToggle")))//Show/hide Graphics/UI settings. + if (GUI.Button(SLineRect(++line), $"{(BDArmorySettings.GRAPHICS_UI_SETTINGS_TOGGLE ? StringUtils.Localize("#LOC_BDArmory_Generic_Hide") : StringUtils.Localize("#LOC_BDArmory_Generic_Show"))} {StringUtils.Localize("#LOC_BDArmory_Settings_GraphicsSettingsToggle")}"))//Show/hide Graphics/UI settings. { - BDArmorySettings.GRAPHICS_UI_SECTION_TOGGLE = !BDArmorySettings.GRAPHICS_UI_SECTION_TOGGLE; + BDArmorySettings.GRAPHICS_UI_SETTINGS_TOGGLE = !BDArmorySettings.GRAPHICS_UI_SETTINGS_TOGGLE; } - if (BDArmorySettings.GRAPHICS_UI_SECTION_TOGGLE) + if (BDArmorySettings.GRAPHICS_UI_SETTINGS_TOGGLE) { line += 0.2f; - BDArmorySettings.DRAW_AIMERS = GUI.Toggle(SLeftRect(++line), BDArmorySettings.DRAW_AIMERS, Localizer.Format("#LOC_BDArmory_Settings_DrawAimers"));//"Draw Aimers" + BDArmorySettings.DRAW_AIMERS = GUI.Toggle(SLeftRect(++line), BDArmorySettings.DRAW_AIMERS, StringUtils.Localize("#LOC_BDArmory_Settings_DrawAimers"));//"Draw Aimers" - if (!BDArmorySettings.ADVANDED_USER_SETTINGS) + if (!BDArmorySettings.ADVANCED_USER_SETTINGS) { - BDArmorySettings.BULLET_HITS = GUI.Toggle(SRightRect(line), BDArmorySettings.BULLET_HITS, Localizer.Format("#LOC_BDArmory_Settings_BulletFX"));//"Bullet Hits" + BDArmorySettings.BULLET_HITS = GUI.Toggle(SRightRect(line), BDArmorySettings.BULLET_HITS, StringUtils.Localize("#LOC_BDArmory_Settings_BulletFX"));//"Bullet Hits" BDArmorySettings.BULLET_DECALS = BDArmorySettings.BULLET_HITS; BDArmorySettings.EJECT_SHELLS = BDArmorySettings.BULLET_HITS; BDArmorySettings.SHELL_COLLISIONS = BDArmorySettings.BULLET_HITS; } else { - BDArmorySettings.BULLET_HITS = GUI.Toggle(SLeftRect(++line), BDArmorySettings.BULLET_HITS, Localizer.Format("#LOC_BDArmory_Settings_BulletHits"));//"Bullet Hits" + BDArmorySettings.BULLET_HITS = GUI.Toggle(SLeftRect(++line), BDArmorySettings.BULLET_HITS, StringUtils.Localize("#LOC_BDArmory_Settings_BulletHits"));//"Bullet Hits" if (BDArmorySettings.BULLET_HITS) { - BDArmorySettings.BULLET_DECALS = GUI.Toggle(SLeftRect(++line, 1), BDArmorySettings.BULLET_DECALS, Localizer.Format("#LOC_BDArmory_Settings_BulletHoleDecals"));//"Bullet Hole Decals" + BDArmorySettings.BULLET_DECALS = GUI.Toggle(SLeftRect(++line, 1), BDArmorySettings.BULLET_DECALS, StringUtils.Localize("#LOC_BDArmory_Settings_BulletHoleDecals"));//"Bullet Hole Decals" if (BDArmorySettings.BULLET_HITS) { - GUI.Label(SLeftSliderRect(++line, 1), $"{Localizer.Format("#LOC_BDArmory_Settings_MaxBulletHoles")}: ({BDArmorySettings.MAX_NUM_BULLET_DECALS})", leftLabel); // Max Bullet Holes + GUI.Label(SLeftSliderRect(++line, 1), $"{StringUtils.Localize("#LOC_BDArmory_Settings_MaxBulletHoles")}: ({BDArmorySettings.MAX_NUM_BULLET_DECALS})", leftLabel); // Max Bullet Holes if (BDArmorySettings.MAX_NUM_BULLET_DECALS != (BDArmorySettings.MAX_NUM_BULLET_DECALS = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.MAX_NUM_BULLET_DECALS, 1f, 999f)))) BulletHitFX.AdjustDecalPoolSizes(BDArmorySettings.MAX_NUM_BULLET_DECALS); } } - BDArmorySettings.EJECT_SHELLS = GUI.Toggle(SLeftRect(++line), BDArmorySettings.EJECT_SHELLS, Localizer.Format("#LOC_BDArmory_Settings_EjectShells"));//"Eject Shells" + BDArmorySettings.EJECT_SHELLS = GUI.Toggle(SLeftRect(++line), BDArmorySettings.EJECT_SHELLS, StringUtils.Localize("#LOC_BDArmory_Settings_EjectShells"));//"Eject Shells" if (BDArmorySettings.EJECT_SHELLS) { - BDArmorySettings.SHELL_COLLISIONS = GUI.Toggle(SLeftRect(++line, 1), BDArmorySettings.SHELL_COLLISIONS, Localizer.Format("#LOC_BDArmory_Settings_ShellCollisions"));//"Shell Collisions"} + BDArmorySettings.SHELL_COLLISIONS = GUI.Toggle(SLeftRect(++line, 1), BDArmorySettings.SHELL_COLLISIONS, StringUtils.Localize("#LOC_BDArmory_Settings_ShellCollisions"));//"Shell Collisions"} } } - BDArmorySettings.SHOW_AMMO_GAUGES = GUI.Toggle(SLeftRect(++line), BDArmorySettings.SHOW_AMMO_GAUGES, Localizer.Format("#LOC_BDArmory_Settings_AmmoGauges"));//"Ammo Gauges" - //BDArmorySettings.PERSISTENT_FX = GUI.Toggle(SRightRect(line), BDArmorySettings.PERSISTENT_FX, Localizer.Format("#LOC_BDArmory_Settings_PersistentFX"));//"Persistent FX" - BDArmorySettings.STRICT_WINDOW_BOUNDARIES = GUI.Toggle(SLeftRect(++line), BDArmorySettings.STRICT_WINDOW_BOUNDARIES, Localizer.Format("#LOC_BDArmory_Settings_StrictWindowBoundaries"));//"Strict Window Boundaries" - if (BDArmorySettings.AI_TOOLBAR_BUTTON != (BDArmorySettings.AI_TOOLBAR_BUTTON = GUI.Toggle(SRightRect(line), BDArmorySettings.AI_TOOLBAR_BUTTON, Localizer.Format("#LOC_BDArmory_Settings_AIToolbarButton")))) // AI Toobar Button + BDArmorySettings.SHOW_AMMO_GAUGES = GUI.Toggle(SLeftRect(++line), BDArmorySettings.SHOW_AMMO_GAUGES, StringUtils.Localize("#LOC_BDArmory_Settings_AmmoGauges"));//"Ammo Gauges" + //BDArmorySettings.PERSISTENT_FX = GUI.Toggle(SRightRect(line), BDArmorySettings.PERSISTENT_FX, StringUtils.Localize("#LOC_BDArmory_Settings_PersistentFX"));//"Persistent FX" + BDArmorySettings.GAPLESS_PARTICLE_EMITTERS = GUI.Toggle(SLeftRect(++line), BDArmorySettings.GAPLESS_PARTICLE_EMITTERS, StringUtils.Localize("#LOC_BDArmory_Settings_GaplessParticleEmitters"));//"Gapless Particle Emitters" + if (BDArmorySettings.FLARE_SMOKE != (BDArmorySettings.FLARE_SMOKE = GUI.Toggle(SRightRect(line), BDArmorySettings.FLARE_SMOKE, StringUtils.Localize("#LOC_BDArmory_Settings_FlareSmoke"))))//"Flare Smoke" + { + foreach (var flareObj in CMDropper.flarePool.pool) + if (flareObj.activeInHierarchy) + { + var flare = flareObj.GetComponent(); + if (flare == null) continue; + flare.EnableEmitters(); + } + } + BDArmorySettings.STRICT_WINDOW_BOUNDARIES = GUI.Toggle(SLeftRect(++line), BDArmorySettings.STRICT_WINDOW_BOUNDARIES, StringUtils.Localize("#LOC_BDArmory_Settings_StrictWindowBoundaries"));//"Strict Window Boundaries" + if (BDArmorySettings.AI_TOOLBAR_BUTTON != (BDArmorySettings.AI_TOOLBAR_BUTTON = GUI.Toggle(SRightRect(line), BDArmorySettings.AI_TOOLBAR_BUTTON, StringUtils.Localize("#LOC_BDArmory_Settings_AIToolbarButton")))) // AI Toobar Button { if (BDArmorySettings.AI_TOOLBAR_BUTTON) { BDArmoryAIGUI.Instance.AddToolbarButton(); } else { BDArmoryAIGUI.Instance.RemoveToolbarButton(); } } - BDArmorySettings.DISPLAY_COMPETITION_STATUS = GUI.Toggle(SLeftRect(++line), BDArmorySettings.DISPLAY_COMPETITION_STATUS, Localizer.Format("#LOC_BDArmory_Settings_DisplayCompetitionStatus")); + if (BDArmorySettings.SCROLL_ZOOM_PREVENTION != (BDArmorySettings.SCROLL_ZOOM_PREVENTION = GUI.Toggle(SLeftRect(++line), BDArmorySettings.SCROLL_ZOOM_PREVENTION, StringUtils.Localize("#LOC_BDArmory_Settings_ScrollZoomPrevention")))) + { GUIUtils.EndDisableScrollZoom(); } + if (BDArmorySettings.VM_TOOLBAR_BUTTON != (BDArmorySettings.VM_TOOLBAR_BUTTON = GUI.Toggle(SRightRect(line), BDArmorySettings.VM_TOOLBAR_BUTTON, StringUtils.Localize("#LOC_BDArmory_Settings_VMToolbarButton")))) // VM Toobar Button + { + if (BDArmorySettings.VM_TOOLBAR_BUTTON) + { VesselMover.Instance.AddToolbarButton(); } + else + { VesselMover.Instance.RemoveToolbarButton(); } + } + BDArmorySettings.DISPLAY_COMPETITION_STATUS = GUI.Toggle(SLeftRect(++line), BDArmorySettings.DISPLAY_COMPETITION_STATUS, StringUtils.Localize("#LOC_BDArmory_Settings_DisplayCompetitionStatus")); + BDArmorySettings.AUTO_DISABLE_UI = GUI.Toggle(SRightRect(line), BDArmorySettings.AUTO_DISABLE_UI, StringUtils.Localize("#LOC_BDArmory_Settings_AutoDisableUI")); // Auto-disable UI if (BDArmorySettings.DISPLAY_COMPETITION_STATUS) { - BDArmorySettings.DISPLAY_COMPETITION_STATUS_WITH_HIDDEN_UI = GUI.Toggle(SLeftRect(++line, 1), BDArmorySettings.DISPLAY_COMPETITION_STATUS_WITH_HIDDEN_UI, Localizer.Format("#LOC_BDArmory_Settings_DisplayCompetitionStatusHiddenUI")); + BDArmorySettings.DISPLAY_COMPETITION_STATUS_WITH_HIDDEN_UI = GUI.Toggle(SLeftRect(++line, 1), BDArmorySettings.DISPLAY_COMPETITION_STATUS_WITH_HIDDEN_UI, StringUtils.Localize("#LOC_BDArmory_Settings_DisplayCompetitionStatusHiddenUI")); } - if (HighLogic.LoadedSceneIsEditor && BDArmorySettings.ADVANDED_USER_SETTINGS) + BDArmorySettings.CAMERA_SWITCH_INCLUDE_MISSILES = GUI.Toggle(SLeftRect(++line), BDArmorySettings.CAMERA_SWITCH_INCLUDE_MISSILES, StringUtils.Localize("#LOC_BDArmory_Settings_CameraSwitchIncludeMissiles")); + if (HighLogic.LoadedSceneIsEditor && BDArmorySettings.ADVANCED_USER_SETTINGS) { - if (BDArmorySettings.SHOW_CATEGORIES != (BDArmorySettings.SHOW_CATEGORIES = GUI.Toggle(SLeftRect(++line), BDArmorySettings.SHOW_CATEGORIES, Localizer.Format("#LOC_BDArmory_Settings_ShowEditorSubcategories"))))//"Show Editor Subcategories" + if (BDArmorySettings.SHOW_CATEGORIES != (BDArmorySettings.SHOW_CATEGORIES = GUI.Toggle(SLeftRect(++line), BDArmorySettings.SHOW_CATEGORIES, StringUtils.Localize("#LOC_BDArmory_Settings_ShowEditorSubcategories"))))//"Show Editor Subcategories" { KSP.UI.Screens.PartCategorizer.Instance.editorPartList.Refresh(); } - if (BDArmorySettings.AUTOCATEGORIZE_PARTS != (BDArmorySettings.AUTOCATEGORIZE_PARTS = GUI.Toggle(SRightRect(line), BDArmorySettings.AUTOCATEGORIZE_PARTS, Localizer.Format("#LOC_BDArmory_Settings_AutocategorizeParts"))))//"Autocategorize Parts" + if (BDArmorySettings.AUTOCATEGORIZE_PARTS != (BDArmorySettings.AUTOCATEGORIZE_PARTS = GUI.Toggle(SRightRect(line), BDArmorySettings.AUTOCATEGORIZE_PARTS, StringUtils.Localize("#LOC_BDArmory_Settings_AutocategorizeParts"))))//"Autocategorize Parts" { KSP.UI.Screens.PartCategorizer.Instance.editorPartList.Refresh(); } } - BDArmorySettings.DRAW_DEBUG_LABELS = GUI.Toggle(SLineThirdRect(++line, 0), BDArmorySettings.DRAW_DEBUG_LABELS, Localizer.Format("#LOC_BDArmory_Settings_DebugLabels"));//"Debug Labels" - if (BDArmorySettings.ADVANDED_USER_SETTINGS) + if (BDArmorySettings.ADVANCED_USER_SETTINGS) { - BDArmorySettings.DRAW_ARMOR_LABELS = GUI.Toggle(SLineThirdRect(line, 1), BDArmorySettings.DRAW_ARMOR_LABELS, Localizer.Format("#LOC_BDArmory_Settings_DebugArmor"));//"Armor debug Lines" - BDArmorySettings.DRAW_DEBUG_LINES = GUI.Toggle(SLineThirdRect(line, 2), BDArmorySettings.DRAW_DEBUG_LINES, Localizer.Format("#LOC_BDArmory_Settings_DebugLines"));//"Debug Lines" + { // GUI background opacity + GUI.Label(SLeftSliderRect(++line), StringUtils.Localize("#LOC_BDArmory_Settings_GUIBackgroundOpacity") + $" ({BDArmorySettings.GUI_OPACITY.ToString("F2")})", leftLabel); + BDArmorySettings.GUI_OPACITY = BDAMath.RoundToUnit(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.GUI_OPACITY, 0f, 1f), 0.05f); + } + + if (GUI.Button(SLineRect(++line, 1, true), (BDArmorySettings.DEBUG_SETTINGS_TOGGLE ? "Disable " : "Enable ") + StringUtils.Localize("#LOC_BDArmory_Settings_DebugSettingsToggle")))//Enable/Disable Debugging. + { + BDArmorySettings.DEBUG_SETTINGS_TOGGLE = !BDArmorySettings.DEBUG_SETTINGS_TOGGLE; + if (!BDArmorySettings.DEBUG_SETTINGS_TOGGLE) // Disable all debugging when closing the debugging section. + { + BDArmorySettings.DEBUG_AI = false; + BDArmorySettings.DEBUG_ARMOR = false; + BDArmorySettings.DEBUG_COMPETITION = false; + BDArmorySettings.DEBUG_DAMAGE = false; + BDArmorySettings.DEBUG_LINES = false; + BDArmorySettings.DEBUG_MISSILES = false; + BDArmorySettings.DEBUG_OTHER = false; + BDArmorySettings.DEBUG_RADAR = false; + BDArmorySettings.DEBUG_SPAWNING = false; + BDArmorySettings.DEBUG_TELEMETRY = false; + BDArmorySettings.DEBUG_WEAPONS = false; + } + } + if (BDArmorySettings.DEBUG_SETTINGS_TOGGLE) + { + BDArmorySettings.DEBUG_TELEMETRY = GUI.Toggle(SQuarterRect(++line, 0, 2), BDArmorySettings.DEBUG_TELEMETRY, StringUtils.Localize("#LOC_BDArmory_Settings_DebugTelemetry"));//"On-Screen Telemetry" + BDArmorySettings.DEBUG_LINES = GUI.Toggle(SQuarterRect(line, 2), BDArmorySettings.DEBUG_LINES, StringUtils.Localize("#LOC_BDArmory_Settings_DebugLines"));//"Debug Lines" + BDArmorySettings.DEBUG_WEAPONS = GUI.Toggle(SQuarterRect(++line, 0), BDArmorySettings.DEBUG_WEAPONS, StringUtils.Localize("#LOC_BDArmory_Settings_DebugWeapons"));//"Debug Weapons" + BDArmorySettings.DEBUG_MISSILES = GUI.Toggle(SQuarterRect(line, 1), BDArmorySettings.DEBUG_MISSILES, StringUtils.Localize("#LOC_BDArmory_Settings_DebugMissiles"));//"Debug Missiles" + BDArmorySettings.DEBUG_ARMOR = GUI.Toggle(SQuarterRect(line, 2), BDArmorySettings.DEBUG_ARMOR, StringUtils.Localize("#LOC_BDArmory_Settings_DebugArmor"));//"Debug Armor" + BDArmorySettings.DEBUG_DAMAGE = GUI.Toggle(SQuarterRect(line, 3), BDArmorySettings.DEBUG_DAMAGE, StringUtils.Localize("#LOC_BDArmory_Settings_DebugDamage"));//"Debug Damage" + BDArmorySettings.DEBUG_AI = GUI.Toggle(SQuarterRect(++line, 0), BDArmorySettings.DEBUG_AI, StringUtils.Localize("#LOC_BDArmory_Settings_DebugAI"));//"Debug AI" + BDArmorySettings.DEBUG_COMPETITION = GUI.Toggle(SQuarterRect(line, 1), BDArmorySettings.DEBUG_COMPETITION, StringUtils.Localize("#LOC_BDArmory_Settings_DebugCompetition"));//"Debug Competition" + BDArmorySettings.DEBUG_RADAR = GUI.Toggle(SQuarterRect(line, 2), BDArmorySettings.DEBUG_RADAR, StringUtils.Localize("#LOC_BDArmory_Settings_DebugRadar"));//"Debug Detectors" + BDArmorySettings.DEBUG_SPAWNING = GUI.Toggle(SQuarterRect(line, 3), BDArmorySettings.DEBUG_SPAWNING, StringUtils.Localize("#LOC_BDArmory_Settings_DebugSpawning"));//"Debug Spawning" + BDArmorySettings.DEBUG_OTHER = GUI.Toggle(SQuarterRect(++line, 0), BDArmorySettings.DEBUG_OTHER, StringUtils.Localize("#LOC_BDArmory_Settings_DebugOther"));//"Debug Other" + + if (BDArmorySettings.DEBUG_OTHER && GUI.Button(SLineRect(++line), StringUtils.Localize("#LOC_BDArmory_Settings_ResetScrollZoom"))) GUIUtils.ResetScrollRate(); // Reset scroll-zoom. + if (BDArmorySettings.DEBUG_AI && GUI.Button(SLineRect(++line), "Debug Extending")) // Debug why a vessel is stuck in extending. + { + var AI = VesselModuleRegistry.GetBDModulePilotAI(FlightGlobals.ActiveVessel); + if (AI is not null) AI.DebugExtending(); + } + if (BDArmorySettings.DEBUG_OTHER && HighLogic.LoadedSceneIsEditor && GUI.Button(SLineRect(++line), "Dump parts")) + { + BDAEditorTools.dumpParts(); + } + } +#if DEBUG // Only visible when compiled in Debug configuration. + if (BDArmorySettings.DEBUG_SETTINGS_TOGGLE) + { + // GUI.Label(SLeftSliderRect(++line), $"Outer loops N ({PROF_N}):"); + // if (PROF_N_pow != (PROF_N_pow = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), PROF_N_pow, 0, 8)))) + // { + // PROF_N = Mathf.RoundToInt(Mathf.Pow(10, PROF_N_pow)); + // } + // GUI.Label(SLeftSliderRect(++line), $"Inner loops n ({PROF_n}):"); + // if (PROF_n_pow != (PROF_n_pow = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), PROF_n_pow, 0, 6)))) + // { + // PROF_n = Mathf.RoundToInt(Mathf.Pow(10, PROF_n_pow)); + // } + + // if (GUI.Button(SLineRect(++line), "Test ProjectOnPlane and PredictPosition")) + // TestProjectOnPlaneAndPredictPosition(); + // if (GUI.Button(SLineRect(++line), "Say hello KAL")) + // { + // foreach (var kal in FlightGlobals.ActiveVessel.FindPartModulesImplementing()) + // { + // if (kal == null) continue; + // Debug.Log($"DEBUG KAL {kal.displayName} found on part {kal.part} ({kal.part.persistentId}), enabled: {kal.controllerEnabled}"); + // Utils.ConfigNodeUtils.PrintConfigNode(kal.snapshot.moduleValues); + // } + // foreach (var part in FlightGlobals.ActiveVessel.Parts) + // { + // if (part == null) continue; + // Debug.Log($"DEBUG Part {part.name} ({part.persistentId})"); + // foreach (var module in part.Modules) + // { + // if (module.PersistentId != 0) Debug.Log($"DEBUG Module {module.name} ({module.PersistentId}, {module.moduleName})"); + // if (module.moduleName == "ModuleRoboticController") foreach (var axis in ((Expansions.Serenity.ModuleRoboticController)module).ControlledAxes) Debug.Log($"DEBUG KAL controls part {axis.PartPersistentId}, module {axis.Module.moduleName} ({axis.Module.PersistentId}), axisField {axis.AxisField.guiName} ({axis.AxisField.name})"); + // } + // var kal = part.FindModuleImplementing(); + // if (kal != null) + // { + // Debug.Log($"DEBUG KAL found on {part.name} ({part.persistentId}))"); + // foreach (var axis in kal.ControlledAxes) Debug.Log($"DEBUG KAL controls part {axis.PartPersistentId}, module {axis.Module.moduleName} ({axis.Module.PersistentId})"); + // } + // } + // } + // GUI.Label(SLeftSliderRect(++line), $"#raycasts {debug_numRaycasts}"); + // debug_numRaycasts = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), debug_numRaycasts, 1, 20)); + // if (GUI.Button(SLineRect(++line), "Test RaycastCommand")) // The break-even appears to be around 8 raycasts. + // { + // int N = 100000; + // Unity.Collections.NativeArray proximityRaycastCommands = new Unity.Collections.NativeArray(debug_numRaycasts, Unity.Collections.Allocator.TempJob); + // Unity.Collections.NativeArray proximityRaycastHits = new Unity.Collections.NativeArray(debug_numRaycasts, Unity.Collections.Allocator.TempJob); // Note: RaycastCommands only return the first hit until Unity 2022.2. + // var vesselPosition = FlightGlobals.ActiveVessel.transform.position; + // var vesselSrfVelDir = FlightGlobals.ActiveVessel.srf_vel_direction; + // var relativeVelocityRightDirection = Vector3.Cross((vesselPosition - FlightGlobals.currentMainBody.transform.position).normalized, vesselSrfVelDir).normalized; + // var relativeVelocityDownDirection = Vector3.Cross(relativeVelocityRightDirection, vesselSrfVelDir).normalized; + // var terrainAlertDetectionRadius = 3f * FlightGlobals.ActiveVessel.GetRadius(); + // var watch = new System.Diagnostics.Stopwatch(); + // float µsResolution = 1e6f / System.Diagnostics.Stopwatch.Frequency; + // watch.Start(); + // for (int i = 0; i < N; ++i) + // { + // for (int j = 0; j < debug_numRaycasts; ++j) + // proximityRaycastCommands[j] = new RaycastCommand(vesselPosition, vesselSrfVelDir - relativeVelocityDownDirection, terrainAlertDetectionRadius, (int)LayerMasks.Scenery); + // var job = RaycastCommand.ScheduleBatch(proximityRaycastCommands, proximityRaycastHits, 1, default(Unity.Jobs.JobHandle)); + // job.Complete(); // Wait for the job to complete. + // } + // watch.Stop(); + // Debug.Log($"Batch RaycastCommand[{debug_numRaycasts}] took {watch.ElapsedTicks * µsResolution / N:G3}µs"); + // RaycastHit rayHit; + // watch.Reset(); watch.Start(); + // for (int i = 0; i < N; ++i) + // for (int j = 0; j < debug_numRaycasts; ++j) + // Physics.Raycast(new Ray(vesselPosition, (vesselSrfVelDir + relativeVelocityDownDirection).normalized), out rayHit, terrainAlertDetectionRadius, (int)LayerMasks.Scenery); + // watch.Stop(); + // Debug.Log($"{debug_numRaycasts} Raycasts took {watch.ElapsedTicks * µsResolution / N:G3}µs"); + // proximityRaycastCommands.Dispose(); + // proximityRaycastHits.Dispose(); + // } + // if (GUI.Button(SLineRect(++line), "Dump staging")) { var vessel = FlightGlobals.ActiveVessel; if (vessel != null) Debug.Log($"DEBUG {vessel.vesselName} is at stage {vessel.currentStage}, part stages: {string.Join("; ", vessel.parts.Select(p => $"{p}: index: {p.inStageIndex}, offset: {p.stageOffset}, orig: {p.originalStage}, child: {p.childStageOffset}, inv: {p.inverseStage}, default inv: {p.defaultInverseStage}, inv carryover: {p.inverseStageCarryover}, manual: {p.manualStageOffset}, after: {p.stageAfter}, before: {p.stageBefore}"))}"); } + // if (GUI.Button(SLineRect(++line), "Vessel Mass")) + // { + // BDACompetitionMode.Instance.competitionStatus.Add($"{FlightGlobals.ActiveVessel.vesselName} has mass {FlightGlobals.ActiveVessel.GetTotalMass()}t"); + // } + // if (GUI.Button(SLineRect(++line), "Test Collider.ClosestPoint[OnBounds]")) + // { + // var watch = new System.Diagnostics.Stopwatch(); + // float µsResolution = 1e6f / System.Diagnostics.Stopwatch.Frequency; + // int N = 1 << 16; + // Ray ray = FlightCamera.fetch.mainCamera.ViewportPointToRay(new Vector3(0.5f, 0.5f, 0f)); + // int layerMask = (int)(LayerMasks.Parts | LayerMasks.EVA | LayerMasks.Wheels | LayerMasks.Scenery); + // float dist = 10000; + // RaycastHit hit; + // Vector3 closestPoint = default; + // watch.Start(); + // if (Physics.Raycast(ray, out hit, dist, layerMask)) + // { + // watch.Stop(); + // var raycastTicks = watch.ElapsedTicks; + // string raycastString = $"Raycast took {raycastTicks * µsResolution:G3}µs"; + // Part partHit = hit.collider.GetComponentInParent(); + // MeshCollider mcol = null; + // bool isMeshCollider = false; + // bool isNonConvexMeshCollider = false; + // watch.Reset(); watch.Start(); + // for (int i = 0; i < N; ++i) + // { + // mcol = hit.collider as MeshCollider; + // isMeshCollider = mcol != null; + // if (isMeshCollider && !mcol.convex) // non-convex mesh colliders are expensive to use ClosestPoint on. + // { + // isNonConvexMeshCollider = true; + // closestPoint = hit.collider.ClosestPointOnBounds(ray.origin); + // } + // else + // closestPoint = hit.collider.ClosestPoint(ray.origin); + // } + // watch.Stop(); + // Debug.Log($"DEBUG {raycastString}, {(isNonConvexMeshCollider ? "ClosestPointOnBounds" : "ClosestPoint")} ({closestPoint}) on{(isMeshCollider ? $" {(isNonConvexMeshCollider ? "non-" : "")}convex mesh" : "")} collider {hit.collider} from camera ({ray.origin}) took {watch.ElapsedTicks * µsResolution / N:G3}µs{(partHit != null ? $", offset from part ({partHit.name}): {closestPoint - partHit.transform.position}" : "")}, offset from hit: {hit.point - closestPoint}"); + // } + // } + // if (GUI.Button(SLineRect(++line), "Test 2x Raycast vs RaycastNonAlloc")) + // { + // var watch = new System.Diagnostics.Stopwatch(); + // float µsResolution = 1e6f / System.Diagnostics.Stopwatch.Frequency; + // int N = 1 << 20; + // Ray ray = FlightCamera.fetch.mainCamera.ViewportPointToRay(new Vector3(0.5f, 0.5f, 0f)); + // RaycastHit hit; + // RaycastHit[] hits = new RaycastHit[100]; + // int layerMask = (int)(LayerMasks.Parts | LayerMasks.EVA | LayerMasks.Wheels | LayerMasks.Scenery); + // float dist = 10000; + // bool didHit = false; + // watch.Start(); + // for (int i = 0; i < N; ++i) + // { + // didHit = Physics.Raycast(ray, out hit, dist, layerMask); + // didHit = Physics.Raycast(ray, out hit, dist, layerMask); + // } + // watch.Stop(); + // Debug.Log($"DEBUG Raycast 2x (hit? {didHit}) took {watch.ElapsedTicks * µsResolution / N:G3}µs"); + // int hitCount = 0; + // watch.Reset(); watch.Start(); + // for (int i = 0; i < N; ++i) + // hitCount = Physics.RaycastNonAlloc(ray, hits, dist, layerMask); + // watch.Stop(); + // Debug.Log($"DEBUG RaycastNonAlloc ({hitCount} hits) took {watch.ElapsedTicks * µsResolution / N:G3}µs"); + // } + // if (GUI.Button(SLineRect(++line), "Test GetFrameVelocityV3f")) + // { + // var watch = new System.Diagnostics.Stopwatch(); + // float resolution = 1e9f / System.Diagnostics.Stopwatch.Frequency; + // int N = 1000; + // Vector3 frameVelocity; + // watch.Start(); + // for (int i = 0; i < N; ++i) + // frameVelocity = Krakensbane.GetFrameVelocityV3f(); + // watch.Stop(); + // Debug.Log($"DEBUG Getting KbVF took {watch.ElapsedTicks * resolution / N:G3}ns"); + // watch.Reset(); watch.Start(); + // for (int i = 0; i < N; ++i) + // frameVelocity = BDKrakensbane.FrameVelocityV3f; + // watch.Stop(); + // Debug.Log($"DEBUG Using BDKrakensbane took {watch.ElapsedTicks * resolution / N:G3}ns"); + // Vector3d FOOffset; + // watch.Reset(); watch.Start(); + // for (int i = 0; i < N; ++i) + // FOOffset = FloatingOrigin.Offset; + // watch.Stop(); + // Debug.Log($"DEBUG Getting FO.Offset took {watch.ElapsedTicks * resolution / N:G3}ns"); + // watch.Reset(); watch.Start(); + // for (int i = 0; i < N; ++i) + // FOOffset = BDKrakensbane.FloatingOriginOffset; + // watch.Stop(); + // Debug.Log($"DEBUG Using BDKrakensbane took {watch.ElapsedTicks * resolution / N:G3}ns"); + // Vector3d FOOffsetNKb; + // watch.Reset(); watch.Start(); + // for (int i = 0; i < N; ++i) + // FOOffsetNKb = FloatingOrigin.OffsetNonKrakensbane; + // watch.Stop(); + // Debug.Log($"DEBUG Getting FO.OffsetNonKrakensbane took {watch.ElapsedTicks * resolution / N:G3}ns"); + // watch.Reset(); watch.Start(); + // for (int i = 0; i < N; ++i) + // FOOffsetNKb = BDKrakensbane.FloatingOriginOffsetNonKrakensbane; + // watch.Stop(); + // Debug.Log($"DEBUG Using BDKrakensbane took {watch.ElapsedTicks * resolution / N:G3}ns"); + // bool KBIsActive; + // watch.Reset(); watch.Start(); + // for (int i = 0; i < N; ++i) + // KBIsActive = !BDKrakensbane.FloatingOriginOffset.IsZero() || !Krakensbane.GetFrameVelocity().IsZero(); + // watch.Stop(); + // Debug.Log($"DEBUG Getting KB is active took {watch.ElapsedTicks * resolution / N:G3}ns"); + // watch.Reset(); watch.Start(); + // for (int i = 0; i < N; ++i) + // KBIsActive = BDKrakensbane.IsActive; + // watch.Stop(); + // Debug.Log($"DEBUG Using BDKrakensbane took {watch.ElapsedTicks * resolution / N:G3}ns"); + // } + // if (GUI.Button(SLineRect(++line), "Test GetAudioClip")) + // { + // StartCoroutine(TestGetAudioClip()); + // } + // if (GUI.Button(SLineRect(++line), "Test vesselName vs GetName()")) + // { + // StartCoroutine(TestVesselName()); + // } + // if (GUI.Button(SLineRect(++line), "Test RaycastHit merge and sort")) + // { + // StartCoroutine(TestRaycastHitMergeAndSort()); + // } + // if (GUI.Button(SLineRect(++line), "Test Localizer.Format vs StringUtils.Localize")) + // { + // StartCoroutine(TestLocalization()); + // } + // if (GUI.Button(SLineRect(++line), "Test yield wait lengths")) // Test yield wait lengths + // { + // StartCoroutine(TestYieldWaitLengths()); + // } + // if (BDACompetitionMode.Instance != null) + // { + // if (GUI.Button(SLineRect(++line), "Run DEBUG checks"))// Run DEBUG checks + // { + // switch (Event.current.button) + // { + // case 1: // right click + // StartCoroutine(BDACompetitionMode.Instance.CheckGCPerformance()); + // break; + // default: + // BDACompetitionMode.Instance.CleanUpKSPsDeadReferences(); + // BDACompetitionMode.Instance.RunDebugChecks(); + // break; + // } + // } + // if (GUI.Button(SLineRect(++line), "Test Vessel Module Registry")) + // { + // StartCoroutine(VesselModuleRegistry.Instance.PerformanceTest()); + // } + // } + // if (GUI.Button(SLineRect(++line), "timing test")) // Timing tests. + // { + // var test = FlightGlobals.ActiveVessel.transform.position; + // float FiringTolerance = 1f; + // float targetRadius = 20f; + // Vector3 finalAimTarget = new Vector3(10f, 20f, 30f); + // Vector3 pos = new Vector3(2f, 3f, 4f); + // float theta_const = Mathf.Deg2Rad * 1f; + // float test_out = 0f; + // int iters = 10000000; + // var now = Time.realtimeSinceStartup; + // for (int i = 0; i < iters; ++i) + // { + // test_out = i > iters ? 1f : 1f - 0.5f * FiringTolerance * FiringTolerance * targetRadius * targetRadius / (finalAimTarget - pos).sqrMagnitude; + // } + // Debug.Log("DEBUG sqrMagnitude " + (Time.realtimeSinceStartup - now) / iters + "s/iter, out: " + test_out); + // now = Time.realtimeSinceStartup; + // for (int i = 0; i < iters; ++i) + // { + // var theta = FiringTolerance * targetRadius / (finalAimTarget - pos).magnitude + theta_const; + // test_out = i > iters ? 1f : 1f - 0.5f * (theta * theta); + // } + // Debug.Log("DEBUG magnitude " + (Time.realtimeSinceStartup - now) / iters + "s/iter, out: " + test_out); + // } + // if (GUI.Button(SLineRect(++line), "Hash vs SubStr test")) + // { + // var armourParts = PartLoader.LoadedPartsList.Select(p => p.partPrefab.partInfo.name).Where(name => name.ToLower().Contains("armor")).ToHashSet(); + // Debug.Log($"DEBUG Armour parts in game: " + string.Join(", ", armourParts)); + // int N = 1 << 24; + // var tic = Time.realtimeSinceStartup; + // for (int i = 0; i < N; ++i) + // armourParts.Contains("BD.PanelArmor"); + // var dt = Time.realtimeSinceStartup - tic; + // Debug.Log($"DEBUG HashSet lookup took {dt / N:G3}s"); + // var armourPart = "BD.PanelArmor"; + // tic = Time.realtimeSinceStartup; + // for (int i = 0; i < N; ++i) + // armourPart.ToLower().Contains("armor"); + // dt = Time.realtimeSinceStartup - tic; + // Debug.Log($"DEBUG SubStr lookup took {dt / N:G3}s"); + + // // Using an actual part to include the part name access. + // var testPart = PartLoader.LoadedPartsList.Select(p => p.partPrefab).First(); + // ProjectileUtils.IsArmorPart(testPart); // Bootstrap the HashSet + // tic = Time.realtimeSinceStartup; + // for (int i = 0; i < N; ++i) + // ProjectileUtils.IsArmorPart(testPart); + // dt = Time.realtimeSinceStartup - tic; + // Debug.Log($"DEBUG Real part HashSet lookup first part took {dt / N:G3}s"); + // testPart = PartLoader.LoadedPartsList.Select(p => p.partPrefab).Last(); + // tic = Time.realtimeSinceStartup; + // for (int i = 0; i < N; ++i) + // ProjectileUtils.IsArmorPart(testPart); + // dt = Time.realtimeSinceStartup - tic; + // Debug.Log($"DEBUG Real part HashSet lookup last part took {dt / N:G3}s"); + // tic = Time.realtimeSinceStartup; + // for (int i = 0; i < N; ++i) + // testPart.partInfo.name.ToLower().Contains("armor"); + // dt = Time.realtimeSinceStartup - tic; + // Debug.Log($"DEBUG Real part SubStr lookup took {dt / N:G3}s"); + + // } + // if (GUI.Button(SLineRect(++line), "Layer test")) + // { + // for (int i = 0; i < 32; ++i) + // { + // // Vector3 mouseAim = new Vector3(Input.mousePosition.x / Screen.width, Input.mousePosition.y / Screen.height, 0); + // Ray ray = FlightCamera.fetch.mainCamera.ViewportPointToRay(new Vector3(0.5f, 0.5f, 0f)); + // RaycastHit hit; + + // if (Physics.Raycast(ray, out hit, 1000f, (1 << i))) + // { + // var hitPart = hit.collider.gameObject.GetComponentInParent(); + // var hitEVA = hit.collider.gameObject.GetComponentUpwards(); + // var hitBuilding = hit.collider.gameObject.GetComponentUpwards(); + // if (hitEVA != null) hitPart = hitEVA.part; + // if (hitPart != null) Debug.Log($"DEBUG Bitmask at {i} hit {hitPart.name}."); + // else if (hitBuilding != null) Debug.Log($"DEBUG Bitmask at {i} hit {hitBuilding.name}"); + // else Debug.Log($"DEBUG Bitmask at {i} hit {hit.collider.gameObject.name}"); + // } + // } + // } + // if (GUI.Button(SLineRect(++line), "Test vessel position timing.")) + // { StartCoroutine(TestVesselPositionTiming()); } + // if (GUI.Button(SLineRect(++line), "FS engine status")) + // { + // foreach (var vessel in FlightGlobals.VesselsLoaded) + // FireSpitter.CheckStatus(vessel); + // } + // if (GUI.Button(SLineRect(++line), "Quit KSP.")) + // { + // TournamentAutoResume.AutoQuit(0); + // } + } +#endif } line += 0.5f; } - if (GUI.Button(SLineRect(++line), (BDArmorySettings.GAMEPLAY_SETTINGS_TOGGLE ? "Hide " : "Show ") + Localizer.Format("#LOC_BDArmory_Settings_GeneralSettingsToggle")))//Show/hide Gameplay settings. + if (GUI.Button(SLineRect(++line), $"{(BDArmorySettings.GAMEPLAY_SETTINGS_TOGGLE ? StringUtils.Localize("#LOC_BDArmory_Generic_Hide") : StringUtils.Localize("#LOC_BDArmory_Generic_Show"))} {StringUtils.Localize("#LOC_BDArmory_Settings_GeneralSettingsToggle")}"))//Show/hide Gameplay settings. { BDArmorySettings.GAMEPLAY_SETTINGS_TOGGLE = !BDArmorySettings.GAMEPLAY_SETTINGS_TOGGLE; } @@ -2355,9 +2805,9 @@ void WindowSettings(int windowID) { line += 0.2f; - BDArmorySettings.AUTO_ENABLE_VESSEL_SWITCHING = GUI.Toggle(SLeftRect(++line), BDArmorySettings.AUTO_ENABLE_VESSEL_SWITCHING, Localizer.Format("#LOC_BDArmory_Settings_AutoEnableVesselSwitching")); + BDArmorySettings.AUTO_ENABLE_VESSEL_SWITCHING = GUI.Toggle(SLeftRect(++line), BDArmorySettings.AUTO_ENABLE_VESSEL_SWITCHING, StringUtils.Localize("#LOC_BDArmory_Settings_AutoEnableVesselSwitching")); { // Kerbal Safety - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_KerbalSafety")}: ({(KerbalSafetyLevel)BDArmorySettings.KERBAL_SAFETY})", leftLabel); // Kerbal Safety + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_KerbalSafety")}: ({(KerbalSafetyLevel)BDArmorySettings.KERBAL_SAFETY})", leftLabel); // Kerbal Safety if (BDArmorySettings.KERBAL_SAFETY != (BDArmorySettings.KERBAL_SAFETY = BDArmorySettings.KERBAL_SAFETY = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.KERBAL_SAFETY, (float)KerbalSafetyLevel.Off, (float)KerbalSafetyLevel.Full)))) { if (BDArmorySettings.KERBAL_SAFETY != (int)KerbalSafetyLevel.Off) @@ -2371,21 +2821,21 @@ void WindowSettings(int windowID) switch (BDArmorySettings.KERBAL_SAFETY_INVENTORY) { case 1: - inventory = Localizer.Format("#LOC_BDArmory_Settings_KerbalSafetyInventory_ResetDefault"); + inventory = StringUtils.Localize("#LOC_BDArmory_Settings_KerbalSafetyInventory_ResetDefault"); break; case 2: - inventory = Localizer.Format("#LOC_BDArmory_Settings_KerbalSafetyInventory_ChuteOnly"); + inventory = StringUtils.Localize("#LOC_BDArmory_Settings_KerbalSafetyInventory_ChuteOnly"); break; default: - inventory = Localizer.Format("#LOC_BDArmory_Settings_KerbalSafetyInventory_NoChange"); + inventory = StringUtils.Localize("#LOC_BDArmory_Settings_KerbalSafetyInventory_NoChange"); break; } - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_KerbalSafetyInventory")}: ({inventory})", leftLabel); // Kerbal Safety inventory + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_KerbalSafetyInventory")}: ({inventory})", leftLabel); // Kerbal Safety inventory if (BDArmorySettings.KERBAL_SAFETY_INVENTORY != (BDArmorySettings.KERBAL_SAFETY_INVENTORY = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.KERBAL_SAFETY_INVENTORY, 0f, 2f)))) - { KerbalSafetyManager.Instance.ReconfigureInventories(); } + { if (KerbalSafetyManager.Instance is not null) KerbalSafetyManager.Instance.ReconfigureInventories(); } } } - if (BDArmorySettings.HACK_INTAKES != (BDArmorySettings.HACK_INTAKES = GUI.Toggle(SLeftRect(++line), BDArmorySettings.HACK_INTAKES, Localizer.Format("#LOC_BDArmory_Settings_IntakeHack"))))// Hack Intakes + if (BDArmorySettings.HACK_INTAKES != (BDArmorySettings.HACK_INTAKES = GUI.Toggle(SLeftRect(++line), BDArmorySettings.HACK_INTAKES, StringUtils.Localize("#LOC_BDArmory_Settings_IntakeHack"))))// Hack Intakes { if (HighLogic.LoadedSceneIsFlight) { @@ -2409,57 +2859,78 @@ void WindowSettings(int windowID) } } - if (BDArmorySettings.ADVANDED_USER_SETTINGS) + if (BDArmorySettings.ADVANCED_USER_SETTINGS) { - BDArmorySettings.DEFAULT_FFA_TARGETING = GUI.Toggle(SLeftRect(++line), BDArmorySettings.DEFAULT_FFA_TARGETING, Localizer.Format("#LOC_BDArmory_Settings_DefaultFFATargeting"));// Free-for-all combat style - BDArmorySettings.DISABLE_RAMMING = GUI.Toggle(SRightRect(line), BDArmorySettings.DISABLE_RAMMING, Localizer.Format("#LOC_BDArmory_Settings_DisableRamming"));// Disable Ramming - BDArmorySettings.AUTONOMOUS_COMBAT_SEATS = GUI.Toggle(SLeftRect(++line), BDArmorySettings.AUTONOMOUS_COMBAT_SEATS, Localizer.Format("#LOC_BDArmory_Settings_AutonomousCombatSeats")); - BDArmorySettings.DESTROY_UNCONTROLLED_WMS = GUI.Toggle(SRightRect(line), BDArmorySettings.DESTROY_UNCONTROLLED_WMS, Localizer.Format("#LOC_BDArmory_Settings_DestroyWMWhenNotControlled")); - BDArmorySettings.AIM_ASSIST = GUI.Toggle(SLeftRect(++line), BDArmorySettings.AIM_ASSIST, Localizer.Format("#LOC_BDArmory_Settings_AimAssist"));//"Aim Assist" - BDArmorySettings.BOMB_CLEARANCE_CHECK = GUI.Toggle(SRightRect(line), BDArmorySettings.BOMB_CLEARANCE_CHECK, Localizer.Format("#LOC_BDArmory_Settings_ClearanceCheck"));//"Clearance Check" - BDArmorySettings.REMOTE_SHOOTING = GUI.Toggle(SLeftRect(++line), BDArmorySettings.REMOTE_SHOOTING, Localizer.Format("#LOC_BDArmory_Settings_RemoteFiring"));//"Remote Firing" - BDArmorySettings.RESET_HP = GUI.Toggle(SRightRect(line), BDArmorySettings.RESET_HP, Localizer.Format("#LOC_BDArmory_Settings_ResetHP")); - BDArmorySettings.BULLET_WATER_DRAG = GUI.Toggle(SLeftRect(++line), BDArmorySettings.BULLET_WATER_DRAG, Localizer.Format("#LOC_BDArmory_Settings_waterDrag"));// Underwater bullet drag - BDArmorySettings.RESET_ARMOUR = GUI.Toggle(SRightRect(line), BDArmorySettings.RESET_ARMOUR, Localizer.Format("#LOC_BDArmory_Settings_ResetArmor")); - BDArmorySettings.VESSEL_RELATIVE_BULLET_CHECKS = GUI.Toggle(SLeftRect(++line), BDArmorySettings.VESSEL_RELATIVE_BULLET_CHECKS, Localizer.Format("#LOC_BDArmory_Settings_VesselRelativeBulletChecks"));//"Vessel-Relative Bullet Checks" - BDArmorySettings.RESET_HULL = GUI.Toggle(SRightRect(line), BDArmorySettings.RESET_HULL, Localizer.Format("#LOC_BDArmory_Settings_ResetHull")); //Reset Hull - BDArmorySettings.AUTO_LOAD_TO_KSC = GUI.Toggle(SLeftRect(++line), BDArmorySettings.AUTO_LOAD_TO_KSC, Localizer.Format("#LOC_BDArmory_Settings_AutoLoadToKSC")); // Auto-Load To KSC - BDArmorySettings.GENERATE_CLEAN_SAVE = GUI.Toggle(SRightRect(line), BDArmorySettings.GENERATE_CLEAN_SAVE, Localizer.Format("#LOC_BDArmory_Settings_GenerateCleanSave")); // Generate Clean Save - BDArmorySettings.AUTO_RESUME_TOURNAMENT = GUI.Toggle(SLeftRect(++line), BDArmorySettings.AUTO_RESUME_TOURNAMENT, Localizer.Format("#LOC_BDArmory_Settings_AutoResumeTournaments")); // Auto-Resume Tournaments + if (BDArmorySettings.PWING_EDGE_LIFT != (BDArmorySettings.PWING_EDGE_LIFT = GUI.Toggle(SRightRect(line), BDArmorySettings.PWING_EDGE_LIFT, StringUtils.Localize("#LOC_BDArmory_Settings_PWingsHack")))) //Toggle Pwing Edge Lift + { + if (HighLogic.LoadedSceneIsEditor && EditorLogic.fetch.ship is not null) GameEvents.onEditorShipModified.Fire(EditorLogic.fetch.ship); + } + BDArmorySettings.DEFAULT_FFA_TARGETING = GUI.Toggle(SLeftRect(++line), BDArmorySettings.DEFAULT_FFA_TARGETING, StringUtils.Localize("#LOC_BDArmory_Settings_DefaultFFATargeting"));// Free-for-all combat style + if (BDArmorySettings.PWING_THICKNESS_AFFECT_MASS_HP != (BDArmorySettings.PWING_THICKNESS_AFFECT_MASS_HP = GUI.Toggle(SRightRect(line), BDArmorySettings.PWING_THICKNESS_AFFECT_MASS_HP, StringUtils.Localize("#LOC_BDArmory_Settings_PWingsThickHP")))) //Toggle Pwing Thickness based Mass/HP + { + if (HighLogic.LoadedSceneIsEditor && EditorLogic.fetch.ship is not null) GameEvents.onEditorShipModified.Fire(EditorLogic.fetch.ship); + } + BDArmorySettings.AUTONOMOUS_COMBAT_SEATS = GUI.Toggle(SLeftRect(++line), BDArmorySettings.AUTONOMOUS_COMBAT_SEATS, StringUtils.Localize("#LOC_BDArmory_Settings_AutonomousCombatSeats")); + BDArmorySettings.DESTROY_UNCONTROLLED_WMS = GUI.Toggle(SRightRect(line), BDArmorySettings.DESTROY_UNCONTROLLED_WMS, StringUtils.Localize("#LOC_BDArmory_Settings_DestroyWMWhenNotControlled")); + BDArmorySettings.AIM_ASSIST = GUI.Toggle(SLeftRect(++line), BDArmorySettings.AIM_ASSIST, StringUtils.Localize("#LOC_BDArmory_Settings_AimAssist"));//"Aim Assist" + BDArmorySettings.AIM_ASSIST_MODE = GUI.Toggle(SRightRect(line), BDArmorySettings.AIM_ASSIST_MODE, BDArmorySettings.AIM_ASSIST_MODE ? StringUtils.Localize("#LOC_BDArmory_Settings_AimAssistMode_Target") : StringUtils.Localize("#LOC_BDArmory_Settings_AimAssistMode_Aimer"));//"Aim Assist Mode (Target/Aimer)" + BDArmorySettings.REMOTE_SHOOTING = GUI.Toggle(SLeftRect(++line), BDArmorySettings.REMOTE_SHOOTING, StringUtils.Localize("#LOC_BDArmory_Settings_RemoteFiring"));//"Remote Firing" + BDArmorySettings.BOMB_CLEARANCE_CHECK = GUI.Toggle(SRightRect(line), BDArmorySettings.BOMB_CLEARANCE_CHECK, StringUtils.Localize("#LOC_BDArmory_Settings_ClearanceCheck"));//"Clearance Check" + BDArmorySettings.DISABLE_RAMMING = GUI.Toggle(SLeftRect(++line), BDArmorySettings.DISABLE_RAMMING, StringUtils.Localize("#LOC_BDArmory_Settings_DisableRamming"));// Disable Ramming + BDArmorySettings.RESET_HP = GUI.Toggle(SRightRect(line), BDArmorySettings.RESET_HP, StringUtils.Localize("#LOC_BDArmory_Settings_ResetHP")); + BDArmorySettings.BULLET_WATER_DRAG = GUI.Toggle(SLeftRect(++line), BDArmorySettings.BULLET_WATER_DRAG, StringUtils.Localize("#LOC_BDArmory_Settings_waterDrag"));// Underwater bullet drag + BDArmorySettings.RESET_ARMOUR = GUI.Toggle(SRightRect(line), BDArmorySettings.RESET_ARMOUR, StringUtils.Localize("#LOC_BDArmory_Settings_ResetArmor")); + BDArmorySettings.VESSEL_RELATIVE_BULLET_CHECKS = GUI.Toggle(SLeftRect(++line), BDArmorySettings.VESSEL_RELATIVE_BULLET_CHECKS, StringUtils.Localize("#LOC_BDArmory_Settings_VesselRelativeBulletChecks"));//"Vessel-Relative Bullet Checks" + BDArmorySettings.RESET_HULL = GUI.Toggle(SRightRect(line), BDArmorySettings.RESET_HULL, StringUtils.Localize("#LOC_BDArmory_Settings_ResetHull")); //Reset Hull + if (BDArmorySettings.RESTORE_KAL != (BDArmorySettings.RESTORE_KAL = GUI.Toggle(SLeftRect(++line), BDArmorySettings.RESTORE_KAL, StringUtils.Localize("#LOC_BDArmory_Settings_RestoreKAL")))) //Restore KAL + { SpawnUtils.RestoreKALGlobally(BDArmorySettings.RESTORE_KAL); } + BDArmorySettings.AUTO_LOAD_TO_KSC = GUI.Toggle(SLeftRect(++line), BDArmorySettings.AUTO_LOAD_TO_KSC, StringUtils.Localize("#LOC_BDArmory_Settings_AutoLoadToKSC")); // Auto-Load To KSC + BDArmorySettings.GENERATE_CLEAN_SAVE = GUI.Toggle(SRightRect(line), BDArmorySettings.GENERATE_CLEAN_SAVE, StringUtils.Localize("#LOC_BDArmory_Settings_GenerateCleanSave")); // Generate Clean Save + BDArmorySettings.AUTO_RESUME_TOURNAMENT = GUI.Toggle(SLeftRect(++line), BDArmorySettings.AUTO_RESUME_TOURNAMENT, StringUtils.Localize("#LOC_BDArmory_Settings_AutoResumeTournaments")); // Auto-Resume Tournaments if (BDArmorySettings.AUTO_RESUME_TOURNAMENT) { - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_AutoQuitMemoryUsage")}: ({(BDArmorySettings.QUIT_MEMORY_USAGE_THRESHOLD > SystemMaxMemory ? "Off" : $"{BDArmorySettings.QUIT_MEMORY_USAGE_THRESHOLD}GB")})", leftLabel); // Auto-Quit Memory Threshold + BDArmorySettings.AUTO_QUIT_AT_END_OF_TOURNAMENT = GUI.Toggle(SRightRect(line), BDArmorySettings.AUTO_QUIT_AT_END_OF_TOURNAMENT, StringUtils.Localize("#LOC_BDArmory_Settings_AutoQuitAtEndOfTournament")); // Auto Quit At End Of Tournament + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_AutoQuitMemoryUsage")}: ({(BDArmorySettings.QUIT_MEMORY_USAGE_THRESHOLD > SystemMaxMemory ? "Off" : $"{BDArmorySettings.QUIT_MEMORY_USAGE_THRESHOLD}GB")})", leftLabel); // Auto-Quit Memory Threshold BDArmorySettings.QUIT_MEMORY_USAGE_THRESHOLD = Mathf.Round(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.QUIT_MEMORY_USAGE_THRESHOLD, 1f, SystemMaxMemory + 1)); if (BDArmorySettings.QUIT_MEMORY_USAGE_THRESHOLD <= SystemMaxMemory) { - GUI.Label(SLineRect(++line, 1), $"{Localizer.Format("#LOC_BDArmory_Settings_CurrentMemoryUsageEstimate")}: {TournamentAutoResume.memoryUsage:F1}GB / {SystemMaxMemory}GB", leftLabel); + GUI.Label(SLineRect(++line, 1), $"{StringUtils.Localize("#LOC_BDArmory_Settings_CurrentMemoryUsageEstimate")}: {TournamentAutoResume.memoryUsage:F1}GB / {SystemMaxMemory}GB", leftLabel); } } - if (BDArmorySettings.TIME_OVERRIDE != (BDArmorySettings.TIME_OVERRIDE = GUI.Toggle(SLeftRect(++line), BDArmorySettings.TIME_OVERRIDE, Localizer.Format("#LOC_BDArmory_Settings_TimeOverride")))) // Time override. + if (BDArmorySettings.TIME_OVERRIDE != (BDArmorySettings.TIME_OVERRIDE = GUI.Toggle(SLeftRect(++line), BDArmorySettings.TIME_OVERRIDE, StringUtils.Localize("#LOC_BDArmory_Settings_TimeOverride")))) // Time override. { - if (!BDArmorySettings.TIME_OVERRIDE) - { - Time.timeScale = 1f; // Reset the time scale - } - else - { - Time.timeScale = BDArmorySettings.TIME_SCALE; // Set the time scale to our setting. - } + OtherUtils.SetTimeOverride(BDArmorySettings.TIME_OVERRIDE); } if (BDArmorySettings.TIME_OVERRIDE) { - GUI.Label(SLeftSliderRect(++line, 1), $"{Localizer.Format("#LOC_BDArmory_Settings_TimeScale")}; ({BDArmorySettings.TIME_SCALE:G2}x)", leftLabel); - if (BDArmorySettings.TIME_SCALE != (BDArmorySettings.TIME_SCALE = Mathf.Round(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.TIME_SCALE, 0f, 5f) * 10f) / 10f)) + GUI.Label(SLeftSliderRect(++line, 1), $"{StringUtils.Localize("#LOC_BDArmory_Settings_TimeScale")}; ({BDArmorySettings.TIME_SCALE:G2}x)", leftLabel); + if (BDArmorySettings.TIME_SCALE != (BDArmorySettings.TIME_SCALE = BDAMath.RoundToUnit(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.TIME_SCALE, 0f, BDArmorySettings.TIME_SCALE_MAX), BDArmorySettings.TIME_SCALE > 5f ? 1f : 0.1f))) { Time.timeScale = BDArmorySettings.TIME_SCALE; } } + BDArmorySettings.MISSILE_CM_SETTING_TOGGLE = GUI.Toggle(SLineRect(++line), BDArmorySettings.MISSILE_CM_SETTING_TOGGLE, StringUtils.Localize("#LOC_BDArmory_Settings_MissileCMToggle")); + if (BDArmorySettings.MISSILE_CM_SETTING_TOGGLE) + { + BDArmorySettings.ASPECTED_IR_SEEKERS = GUI.Toggle(SLineRect(++line, 1), BDArmorySettings.ASPECTED_IR_SEEKERS, StringUtils.Localize("#LOC_BDArmory_Settings_AspectedIRSeekers")); + GUI.Label(SLeftSliderRect(++line, 1), $"{StringUtils.Localize("#LOC_BDArmory_Settings_FlareFactor")}: ({BDArmorySettings.FLARE_FACTOR})", leftLabel); + BDArmorySettings.FLARE_FACTOR = BDAMath.RoundToUnit(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.FLARE_FACTOR, 0f, 3f), 0.05f); + + GUI.Label(SLeftSliderRect(++line, 1), $"{StringUtils.Localize("#LOC_BDArmory_Settings_ChaffFactor")}: ({BDArmorySettings.CHAFF_FACTOR})", leftLabel); + BDArmorySettings.CHAFF_FACTOR = BDAMath.RoundToUnit(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.CHAFF_FACTOR, 0f, 3f), 0.05f); + + GUI.Label(SLeftSliderRect(++line, 1), $"{StringUtils.Localize("#LOC_BDArmory_Settings_SmokeDeflectionFactor")}: ({BDArmorySettings.SMOKE_DEFLECTION_FACTOR})", leftLabel); + BDArmorySettings.SMOKE_DEFLECTION_FACTOR = BDAMath.RoundToUnit(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.SMOKE_DEFLECTION_FACTOR, 0f, 40f), 0.5f); + + GUI.Label(SLeftSliderRect(++line, 1), $"{StringUtils.Localize("#LOC_BDArmory_Settings_APSThreshold")}: ({BDArmorySettings.APS_THRESHOLD})", leftLabel); + BDArmorySettings.APS_THRESHOLD = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.APS_THRESHOLD, 1f, 356f)); + } } line += 0.5f; } - if (GUI.Button(SLineRect(++line), (BDArmorySettings.SLIDER_SETTINGS_TOGGLE ? "Hide " : "Show ") + Localizer.Format("#LOC_BDArmory_Settings_SliderSettingsToggle")))//Show/hide General Slider settings. + if (GUI.Button(SLineRect(++line), $"{(BDArmorySettings.SLIDER_SETTINGS_TOGGLE ? StringUtils.Localize("#LOC_BDArmory_Generic_Hide") : StringUtils.Localize("#LOC_BDArmory_Generic_Show"))} {StringUtils.Localize("#LOC_BDArmory_Settings_SliderSettingsToggle")}"))//Show/hide General Slider settings. { BDArmorySettings.SLIDER_SETTINGS_TOGGLE = !BDArmorySettings.SLIDER_SETTINGS_TOGGLE; } @@ -2468,66 +2939,86 @@ void WindowSettings(int windowID) line += 0.2f; float dmgMultiplier = BDArmorySettings.DMG_MULTIPLIER <= 100f ? BDArmorySettings.DMG_MULTIPLIER / 10f : BDArmorySettings.DMG_MULTIPLIER / 50f + 8f; - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_DamageMultiplier")}: ({BDArmorySettings.DMG_MULTIPLIER})", leftLabel); // Damage Multiplier + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_DamageMultiplier")}: ({BDArmorySettings.DMG_MULTIPLIER})", leftLabel); // Damage Multiplier dmgMultiplier = Mathf.Round(GUI.HorizontalSlider(SRightSliderRect(line), dmgMultiplier, 1f, 28f)); BDArmorySettings.DMG_MULTIPLIER = dmgMultiplier < 11 ? (int)(dmgMultiplier * 10f) : (int)(50f * (dmgMultiplier - 8f)); - if (BDArmorySettings.ADVANDED_USER_SETTINGS) + if (BDArmorySettings.ADVANCED_USER_SETTINGS) { - BDArmorySettings.EXTRA_DAMAGE_SLIDERS = GUI.Toggle(SLeftRect(++line), BDArmorySettings.EXTRA_DAMAGE_SLIDERS, Localizer.Format("#LOC_BDArmory_Settings_ExtraDamageSliders")); + BDArmorySettings.EXTRA_DAMAGE_SLIDERS = GUI.Toggle(SLeftRect(++line), BDArmorySettings.EXTRA_DAMAGE_SLIDERS, StringUtils.Localize("#LOC_BDArmory_Settings_ExtraDamageSliders")); if (BDArmorySettings.EXTRA_DAMAGE_SLIDERS) { - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_BallisticDamageMultiplier")}: ({BDArmorySettings.BALLISTIC_DMG_FACTOR})", leftLabel); - BDArmorySettings.BALLISTIC_DMG_FACTOR = Mathf.Round((GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.BALLISTIC_DMG_FACTOR * 20f, 0f, 60f))) / 20f; + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_BallisticDamageMultiplier")}: ({BDArmorySettings.BALLISTIC_DMG_FACTOR})", leftLabel); + BDArmorySettings.BALLISTIC_DMG_FACTOR = BDAMath.RoundToUnit(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.BALLISTIC_DMG_FACTOR, 0f, 3f), 0.05f); - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_ExplosiveDamageMultiplier")}: ({BDArmorySettings.EXP_DMG_MOD_BALLISTIC_NEW})", leftLabel); - BDArmorySettings.EXP_DMG_MOD_BALLISTIC_NEW = Mathf.Round((GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.EXP_DMG_MOD_BALLISTIC_NEW * 20f, 0f, 30f))) / 20f; + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_ExplosiveDamageMultiplier")}: ({BDArmorySettings.EXP_DMG_MOD_BALLISTIC_NEW})", leftLabel); + BDArmorySettings.EXP_DMG_MOD_BALLISTIC_NEW = BDAMath.RoundToUnit(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.EXP_DMG_MOD_BALLISTIC_NEW, 0f, 1.5f), 0.05f); - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_RocketExplosiveDamageMultiplier")}: ({BDArmorySettings.EXP_DMG_MOD_ROCKET})", leftLabel); - BDArmorySettings.EXP_DMG_MOD_ROCKET = Mathf.Round((GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.EXP_DMG_MOD_ROCKET * 20f, 0f, 40f))) / 20f; + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_RocketExplosiveDamageMultiplier")}: ({BDArmorySettings.EXP_DMG_MOD_ROCKET})", leftLabel); + BDArmorySettings.EXP_DMG_MOD_ROCKET = BDAMath.RoundToUnit(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.EXP_DMG_MOD_ROCKET, 0f, 2f), 0.05f); - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_MissileExplosiveDamageMultiplier")}: ({BDArmorySettings.EXP_DMG_MOD_MISSILE})", leftLabel); - BDArmorySettings.EXP_DMG_MOD_MISSILE = Mathf.Round((GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.EXP_DMG_MOD_MISSILE * 4f, 0f, 40f))) / 4f; + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_MissileExplosiveDamageMultiplier")}: ({BDArmorySettings.EXP_DMG_MOD_MISSILE})", leftLabel); + BDArmorySettings.EXP_DMG_MOD_MISSILE = BDAMath.RoundToUnit(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.EXP_DMG_MOD_MISSILE, 0f, 10f), 0.25f); - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_ImplosiveDamageMultiplier")}: ({BDArmorySettings.EXP_IMP_MOD})", leftLabel); - BDArmorySettings.EXP_IMP_MOD = Mathf.Round((GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.EXP_IMP_MOD * 20, 0f, 20f))) / 20f; + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_ImplosiveDamageMultiplier")}: ({BDArmorySettings.EXP_IMP_MOD})", leftLabel); + BDArmorySettings.EXP_IMP_MOD = BDAMath.RoundToUnit(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.EXP_IMP_MOD, 0f, 1f), 0.05f); - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_ExplosiveBattleDamageMultiplier")}: ({BDArmorySettings.EXP_DMG_MOD_BATTLE_DAMAGE})", leftLabel); - BDArmorySettings.EXP_DMG_MOD_BATTLE_DAMAGE = Mathf.Round((GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.EXP_DMG_MOD_BATTLE_DAMAGE * 10f, 0f, 20f))) / 10f; - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_SecondaryEffectDuration")}: ({BDArmorySettings.WEAPON_FX_DURATION})", leftLabel); + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_ArmorExplosivePenetrationResistanceMultiplier")}: ({BDArmorySettings.EXP_PEN_RESIST_MULT})", leftLabel); + BDArmorySettings.EXP_PEN_RESIST_MULT = BDAMath.RoundToUnit(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.EXP_PEN_RESIST_MULT, 0f, 10f), 0.25f); + + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_ExplosiveBattleDamageMultiplier")}: ({BDArmorySettings.EXP_DMG_MOD_BATTLE_DAMAGE})", leftLabel); + BDArmorySettings.EXP_DMG_MOD_BATTLE_DAMAGE = BDAMath.RoundToUnit(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.EXP_DMG_MOD_BATTLE_DAMAGE, 0f, 2f), 0.1f); + + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_BuildingDamageMultiplier")}: ({BDArmorySettings.BUILDING_DMG_MULTIPLIER})", leftLabel); + BDArmorySettings.BUILDING_DMG_MULTIPLIER = BDAMath.RoundToUnit((GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.BUILDING_DMG_MULTIPLIER, 0f, 10f)), 0.1f); + + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_SecondaryEffectDuration")}: ({BDArmorySettings.WEAPON_FX_DURATION})", leftLabel); BDArmorySettings.WEAPON_FX_DURATION = Mathf.Round(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.WEAPON_FX_DURATION, 5f, 20f)); - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_BallisticTrajectorSimulationMultiplier")}: ({BDArmorySettings.BALLISTIC_TRAJECTORY_SIMULATION_MULTIPLIER})", leftLabel); + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_BallisticTrajectorSimulationMultiplier")}: ({BDArmorySettings.BALLISTIC_TRAJECTORY_SIMULATION_MULTIPLIER})", leftLabel); BDArmorySettings.BALLISTIC_TRAJECTORY_SIMULATION_MULTIPLIER = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.BALLISTIC_TRAJECTORY_SIMULATION_MULTIPLIER, 1f, 256f)); + + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_ArmorMassMultiplier")}: ({BDArmorySettings.ARMOR_MASS_MOD})", leftLabel); + BDArmorySettings.ARMOR_MASS_MOD = BDAMath.RoundToUnit(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.ARMOR_MASS_MOD, 0.05f, 2f), 0.05f); //armor mult shouldn't be zero, else armor will never take damage, might also break some other things } } // Kill categories - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_Scoring_HeadShot")}: ({BDArmorySettings.SCORING_HEADSHOT}s)", leftLabel); // Scoring head-shot time limit + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_Scoring_HeadShot")}: ({BDArmorySettings.SCORING_HEADSHOT}s)", leftLabel); // Scoring head-shot time limit BDArmorySettings.SCORING_HEADSHOT = Mathf.Round(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.SCORING_HEADSHOT, 1f, 10f)); BDArmorySettings.SCORING_KILLSTEAL = Mathf.Max(BDArmorySettings.SCORING_HEADSHOT, BDArmorySettings.SCORING_KILLSTEAL); - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_Scoring_KillSteal")}: ({BDArmorySettings.SCORING_KILLSTEAL}s)", leftLabel); // Scoring kill-steal time limit + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_Scoring_KillSteal")}: ({BDArmorySettings.SCORING_KILLSTEAL}s)", leftLabel); // Scoring kill-steal time limit BDArmorySettings.SCORING_KILLSTEAL = Mathf.Round(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.SCORING_KILLSTEAL, BDArmorySettings.SCORING_HEADSHOT, 30f)); - if (BDArmorySettings.ADVANDED_USER_SETTINGS) + if (BDArmorySettings.ADVANCED_USER_SETTINGS) { - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_TerrainAlertFrequency")}: ({BDArmorySettings.TERRAIN_ALERT_FREQUENCY})", leftLabel); // Terrain alert frequency. Note: this is scaled by (int)(1+(radarAlt/500)^2) to avoid wasting too many cycles. + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_TerrainAlertFrequency")}: ({BDArmorySettings.TERRAIN_ALERT_FREQUENCY})", leftLabel); // Terrain alert frequency. Note: this is scaled by (int)(1+(radarAlt/500)^2) to avoid wasting too many cycles. BDArmorySettings.TERRAIN_ALERT_FREQUENCY = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.TERRAIN_ALERT_FREQUENCY, 1f, 5f)); } - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_CameraSwitchFrequency")}: ({BDArmorySettings.CAMERA_SWITCH_FREQUENCY}s)", leftLabel); // Minimum camera switching frequency + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_CameraSwitchFrequency")}: ({BDArmorySettings.CAMERA_SWITCH_FREQUENCY}s)", leftLabel); // Minimum camera switching frequency BDArmorySettings.CAMERA_SWITCH_FREQUENCY = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.CAMERA_SWITCH_FREQUENCY, 1f, 10f)); - if (BDArmorySettings.ADVANDED_USER_SETTINGS) + if (BDArmorySettings.ADVANCED_USER_SETTINGS) { - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_DeathCameraInhibitPeriod")}: ({(BDArmorySettings.DEATH_CAMERA_SWITCH_INHIBIT_PERIOD == 0 ? BDArmorySettings.CAMERA_SWITCH_FREQUENCY / 2f : BDArmorySettings.DEATH_CAMERA_SWITCH_INHIBIT_PERIOD)}s)", leftLabel); // Camera switch inhibit period after the active vessel dies. + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_DeathCameraInhibitPeriod")}: ({(BDArmorySettings.DEATH_CAMERA_SWITCH_INHIBIT_PERIOD == 0 ? BDArmorySettings.CAMERA_SWITCH_FREQUENCY / 2f : BDArmorySettings.DEATH_CAMERA_SWITCH_INHIBIT_PERIOD)}s)", leftLabel); // Camera switch inhibit period after the active vessel dies. BDArmorySettings.DEATH_CAMERA_SWITCH_INHIBIT_PERIOD = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.DEATH_CAMERA_SWITCH_INHIBIT_PERIOD, 0f, 10f)); } + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_Max_PWing_HP")}: {(BDArmorySettings.HP_THRESHOLD >= 100 ? (BDArmorySettings.HP_THRESHOLD.ToString()) : "Unclamped")}", leftLabel); // HP Scaling Threshold + if (BDArmorySettings.HP_THRESHOLD != (BDArmorySettings.HP_THRESHOLD = BDAMath.RoundToUnit(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.HP_THRESHOLD, 0, 10000), 100))) + { + if (HighLogic.LoadedSceneIsEditor && EditorLogic.fetch.ship is not null) GameEvents.onEditorShipModified.Fire(EditorLogic.fetch.ship); + } + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_HP_Clamp")}: {(BDArmorySettings.HP_CLAMP >= 100 ? (BDArmorySettings.HP_CLAMP.ToString()) : "Unclamped")}", leftLabel); // HP Scaling Threshold + if (BDArmorySettings.HP_CLAMP != (BDArmorySettings.HP_CLAMP = BDAMath.RoundToUnit(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.HP_CLAMP, 0, 25000), 250))) + { + if (HighLogic.LoadedSceneIsEditor && EditorLogic.fetch.ship is not null) GameEvents.onEditorShipModified.Fire(EditorLogic.fetch.ship); + } line += 0.5f; } - if (GUI.Button(SLineRect(++line), (BDArmorySettings.GAME_MODES_SETTINGS_TOGGLE ? "Hide " : "Show ") + Localizer.Format("#LOC_BDArmory_Settings_GameModesSettingsToggle")))//Show/hide Game Modes settings. + if (GUI.Button(SLineRect(++line), $"{(BDArmorySettings.GAME_MODES_SETTINGS_TOGGLE ? StringUtils.Localize("#LOC_BDArmory_Generic_Hide") : StringUtils.Localize("#LOC_BDArmory_Generic_Show"))} {StringUtils.Localize("#LOC_BDArmory_Settings_GameModesSettingsToggle")}"))//Show/hide Game Modes settings. { BDArmorySettings.GAME_MODES_SETTINGS_TOGGLE = !BDArmorySettings.GAME_MODES_SETTINGS_TOGGLE; } @@ -2535,15 +3026,11 @@ void WindowSettings(int windowID) { line += 0.2f; - BDArmorySettings.BATTLEDAMAGE = GUI.Toggle(SLeftRect(++line), BDArmorySettings.BATTLEDAMAGE, Localizer.Format("#LOC_BDArmory_Settings_BattleDamage")); - BDArmorySettings.INFINITE_AMMO = GUI.Toggle(SRightRect(line), BDArmorySettings.INFINITE_AMMO, Localizer.Format("#LOC_BDArmory_Settings_InfiniteAmmo"));//"Infinite Ammo" - BDArmorySettings.TAG_MODE = GUI.Toggle(SLeftRect(++line), BDArmorySettings.TAG_MODE, Localizer.Format("#LOC_BDArmory_Settings_TagMode"));//"Tag Mode" - if (BDArmorySettings.PAINTBALL_MODE != (BDArmorySettings.PAINTBALL_MODE = GUI.Toggle(SRightRect(line), BDArmorySettings.PAINTBALL_MODE, Localizer.Format("#LOC_BDArmory_Settings_PaintballMode"))))//"Paintball Mode" - { - BulletHitFX.SetupShellPool(); - BDArmorySettings.BATTLEDAMAGE = false; - } - if (BDArmorySettings.GRAVITY_HACKS != (BDArmorySettings.GRAVITY_HACKS = GUI.Toggle(SLeftRect(++line), BDArmorySettings.GRAVITY_HACKS, Localizer.Format("#LOC_BDArmory_Settings_GravityHacks"))))//"Gravity hacks" + BDArmorySettings.BATTLEDAMAGE = GUI.Toggle(SLeftRect(++line), BDArmorySettings.BATTLEDAMAGE, StringUtils.Localize("#LOC_BDArmory_Settings_BattleDamage")); + BDArmorySettings.INFINITE_AMMO = GUI.Toggle(SRightRect(line), BDArmorySettings.INFINITE_AMMO, StringUtils.Localize("#LOC_BDArmory_Settings_InfiniteAmmo"));//"Infinite Ammo" + BDArmorySettings.TAG_MODE = GUI.Toggle(SLeftRect(++line), BDArmorySettings.TAG_MODE, StringUtils.Localize("#LOC_BDArmory_Settings_TagMode"));//"Tag Mode" + BDArmorySettings.INFINITE_ORDINANCE = GUI.Toggle(SRightRect(line), BDArmorySettings.INFINITE_ORDINANCE, StringUtils.Localize("#LOC_BDArmory_Settings_InfiniteMissiles"));//"Infinite Ammo" + if (BDArmorySettings.GRAVITY_HACKS != (BDArmorySettings.GRAVITY_HACKS = GUI.Toggle(SLeftRect(++line), BDArmorySettings.GRAVITY_HACKS, StringUtils.Localize("#LOC_BDArmory_Settings_GravityHacks"))))//"Gravity hacks" { if (BDArmorySettings.GRAVITY_HACKS) { @@ -2558,17 +3045,23 @@ void WindowSettings(int windowID) VehiclePhysics.Gravity.Refresh(); } } - if (BDArmorySettings.PEACE_MODE != (BDArmorySettings.PEACE_MODE = GUI.Toggle(SRightRect(line), BDArmorySettings.PEACE_MODE, Localizer.Format("#LOC_BDArmory_Settings_PeaceMode"))))//"Peace Mode" + if (BDArmorySettings.PAINTBALL_MODE != (BDArmorySettings.PAINTBALL_MODE = GUI.Toggle(SRightRect(line), BDArmorySettings.PAINTBALL_MODE, StringUtils.Localize("#LOC_BDArmory_Settings_PaintballMode"))))//"Paintball Mode" + { + BulletHitFX.SetupShellPool(); + BDArmorySettings.BATTLEDAMAGE = false; + } + if (BDArmorySettings.PEACE_MODE != (BDArmorySettings.PEACE_MODE = GUI.Toggle(SRightRect(++line), BDArmorySettings.PEACE_MODE, StringUtils.Localize("#LOC_BDArmory_Settings_PeaceMode"))))//"Peace Mode" { BDATargetManager.ClearDatabase(); if (OnPeaceEnabled != null) { OnPeaceEnabled(); } + CheatOptions.InfinitePropellant = BDArmorySettings.PEACE_MODE || BDArmorySettings.INFINITE_FUEL; } //Mutators var oldMutators = BDArmorySettings.MUTATOR_MODE; - BDArmorySettings.MUTATOR_MODE = GUI.Toggle(SLeftRect(++line), BDArmorySettings.MUTATOR_MODE, Localizer.Format("#LOC_BDArmory_Settings_Mutators")); + BDArmorySettings.MUTATOR_MODE = GUI.Toggle(SLeftRect(line), BDArmorySettings.MUTATOR_MODE, StringUtils.Localize("#LOC_BDArmory_Settings_Mutators")); { if (BDArmorySettings.MUTATOR_MODE) { @@ -2582,7 +3075,7 @@ void WindowSettings(int windowID) } } } - selectMutators = GUI.Toggle(SLeftRect(++line, 1f), selectMutators, Localizer.Format("#LOC_BDArmory_MutatorSelect")); + selectMutators = GUI.Toggle(SLeftRect(++line, 1f), selectMutators, StringUtils.Localize("#LOC_BDArmory_MutatorSelect")); if (selectMutators) { ++line; @@ -2612,12 +3105,12 @@ void WindowSettings(int windowID) GUI.EndScrollView(); line += 6.5f; - if (GUI.Button(SRightRect(line), Localizer.Format("#LOC_BDArmory_reset"))) + if (GUI.Button(SRightRect(line), StringUtils.Localize("#LOC_BDArmory_reset"))) { switch (Event.current.button) { case 1: // right click - Debug.Log("[BDArmory.BDArmorySetup]: MutatorList: " + string.Join("; ", BDArmorySettings.MUTATOR_LIST)); + Debug.Log($"[BDArmory.BDArmorySetup]: MutatorList: {string.Join("; ", BDArmorySettings.MUTATOR_LIST)}"); break; default: BDArmorySettings.MUTATOR_LIST.Clear(); @@ -2628,12 +3121,12 @@ void WindowSettings(int windowID) } line += .2f; } - BDArmorySettings.MUTATOR_APPLY_GLOBAL = GUI.Toggle(SLeftRect(++line, 1f), BDArmorySettings.MUTATOR_APPLY_GLOBAL, Localizer.Format("#LOC_BDArmory_Settings_MutatorGlobal")); + BDArmorySettings.MUTATOR_APPLY_GLOBAL = GUI.Toggle(SLeftRect(++line, 1f), BDArmorySettings.MUTATOR_APPLY_GLOBAL, StringUtils.Localize("#LOC_BDArmory_Settings_MutatorGlobal")); if (BDArmorySettings.MUTATOR_APPLY_GLOBAL) //if more than 1 mutator selected, will shuffle each round { BDArmorySettings.MUTATOR_APPLY_KILL = false; } - BDArmorySettings.MUTATOR_APPLY_KILL = GUI.Toggle(SRightRect(line, 1f), BDArmorySettings.MUTATOR_APPLY_KILL, Localizer.Format("#LOC_BDArmory_Settings_MutatorKill")); + BDArmorySettings.MUTATOR_APPLY_KILL = GUI.Toggle(SRightRect(line, 1f), BDArmorySettings.MUTATOR_APPLY_KILL, StringUtils.Localize("#LOC_BDArmory_Settings_MutatorKill")); if (BDArmorySettings.MUTATOR_APPLY_KILL) // if more than 1 mutator selected, will randomly assign mutator on kill { BDArmorySettings.MUTATOR_APPLY_GLOBAL = false; @@ -2643,7 +3136,7 @@ void WindowSettings(int windowID) if (BDArmorySettings.MUTATOR_LIST.Count > 1) { - BDArmorySettings.MUTATOR_APPLY_TIMER = GUI.Toggle(SLeftRect(++line, 1f), BDArmorySettings.MUTATOR_APPLY_TIMER, Localizer.Format("#LOC_BDArmory_Settings_MutatorTimed")); + BDArmorySettings.MUTATOR_APPLY_TIMER = GUI.Toggle(SLeftRect(++line, 1f), BDArmorySettings.MUTATOR_APPLY_TIMER, StringUtils.Localize("#LOC_BDArmory_Settings_MutatorTimed")); if (BDArmorySettings.MUTATOR_APPLY_TIMER) //only an option if more than one mutator selected { BDArmorySettings.MUTATOR_APPLY_KILL = false; @@ -2659,10 +3152,10 @@ void WindowSettings(int windowID) BDArmorySettings.MUTATOR_APPLY_GLOBAL = true; } - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_MutatorDuration")}: ({(BDArmorySettings.MUTATOR_DURATION > 0 ? BDArmorySettings.MUTATOR_DURATION + (BDArmorySettings.MUTATOR_DURATION > 1 ? " mins" : " min") : "Unlimited")})", leftLabel); + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_MutatorDuration")}: ({(BDArmorySettings.MUTATOR_DURATION > 0 ? BDArmorySettings.MUTATOR_DURATION + (BDArmorySettings.MUTATOR_DURATION > 1 ? " mins" : " min") : "Unlimited")})", leftLabel); BDArmorySettings.MUTATOR_DURATION = (float)Math.Round(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.MUTATOR_DURATION, 0f, BDArmorySettings.COMPETITION_DURATION), 1); - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_MutatorNum")}: ({BDArmorySettings.MUTATOR_APPLY_NUM})", leftLabel);//Number of active mutators + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_MutatorNum")}: ({BDArmorySettings.MUTATOR_APPLY_NUM})", leftLabel);//Number of active mutators BDArmorySettings.MUTATOR_APPLY_NUM = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.MUTATOR_APPLY_NUM, 1f, BDArmorySettings.MUTATOR_LIST.Count)); if (BDArmorySettings.MUTATOR_LIST.Count < BDArmorySettings.MUTATOR_APPLY_NUM) { @@ -2672,54 +3165,80 @@ void WindowSettings(int windowID) { BDArmorySettings.MUTATOR_APPLY_NUM = 1; } - BDArmorySettings.MUTATOR_ICONS = GUI.Toggle(SLeftRect(++line, 1f), BDArmorySettings.MUTATOR_ICONS, Localizer.Format("#LOC_BDArmory_Settings_MutatorIcons")); + BDArmorySettings.MUTATOR_ICONS = GUI.Toggle(SLeftRect(++line, 1f), BDArmorySettings.MUTATOR_ICONS, StringUtils.Localize("#LOC_BDArmory_Settings_MutatorIcons")); } } + BDArmorySettings.INFINITE_FUEL = CheatOptions.InfinitePropellant; // Sync with the Alt-F12 window if the checkbox was toggled there. + if (BDArmorySettings.INFINITE_FUEL != (BDArmorySettings.INFINITE_FUEL = GUI.Toggle(SRightRect(++line), BDArmorySettings.INFINITE_FUEL, StringUtils.Localize("#autoLOC_900349"))))//"Infinite Propellant" + { + CheatOptions.InfinitePropellant = BDArmorySettings.INFINITE_FUEL; + } // Heartbleed - BDArmorySettings.HEART_BLEED_ENABLED = GUI.Toggle(SLeftRect(++line), BDArmorySettings.HEART_BLEED_ENABLED, Localizer.Format("#LOC_BDArmory_Settings_HeartBleed"));//"Heart Bleed" + BDArmorySettings.HEART_BLEED_ENABLED = GUI.Toggle(SLeftRect(line), BDArmorySettings.HEART_BLEED_ENABLED, StringUtils.Localize("#LOC_BDArmory_Settings_HeartBleed"));//"Heart Bleed" if (BDArmorySettings.HEART_BLEED_ENABLED) { - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_HeartBleedRate")}: ({BDArmorySettings.HEART_BLEED_RATE})", leftLabel);//Heart Bleed Rate + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_HeartBleedRate")}: ({BDArmorySettings.HEART_BLEED_RATE})", leftLabel);//Heart Bleed Rate BDArmorySettings.HEART_BLEED_RATE = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.HEART_BLEED_RATE, 0f, 0.1f) * 1000f) / 1000f; - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_HeartBleedInterval")}: ({BDArmorySettings.HEART_BLEED_INTERVAL})", leftLabel);//Heart Bleed Interval + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_HeartBleedInterval")}: ({BDArmorySettings.HEART_BLEED_INTERVAL})", leftLabel);//Heart Bleed Interval BDArmorySettings.HEART_BLEED_INTERVAL = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.HEART_BLEED_INTERVAL, 1f, 60f)); - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_HeartBleedThreshold")}: ({BDArmorySettings.HEART_BLEED_THRESHOLD})", leftLabel);//Heart Bleed Threshold + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_HeartBleedThreshold")}: ({BDArmorySettings.HEART_BLEED_THRESHOLD})", leftLabel);//Heart Bleed Threshold BDArmorySettings.HEART_BLEED_THRESHOLD = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.HEART_BLEED_THRESHOLD, 1f, 100f)); } + BDArmorySettings.INFINITE_EC = CheatOptions.InfiniteElectricity; // Sync with the Alt-F12 window if the checkbox was toggled there. + if (BDArmorySettings.INFINITE_EC != (BDArmorySettings.INFINITE_EC = GUI.Toggle(SRightRect(++line), BDArmorySettings.INFINITE_EC, StringUtils.Localize("#autoLOC_900361"))))//"Infinite Electricity" + { + CheatOptions.InfiniteElectricity = BDArmorySettings.INFINITE_EC; + } // Resource steal - BDArmorySettings.RESOURCE_STEAL_ENABLED = GUI.Toggle(SLeftRect(++line), BDArmorySettings.RESOURCE_STEAL_ENABLED, Localizer.Format("#LOC_BDArmory_Settings_ResourceSteal"));//"Resource Steal" + BDArmorySettings.RESOURCE_STEAL_ENABLED = GUI.Toggle(SLeftRect(line), BDArmorySettings.RESOURCE_STEAL_ENABLED, StringUtils.Localize("#LOC_BDArmory_Settings_ResourceSteal"));//"Resource Steal" if (BDArmorySettings.RESOURCE_STEAL_ENABLED) { - BDArmorySettings.RESOURCE_STEAL_RESPECT_FLOWSTATE_IN = GUI.Toggle(SLeftRect(++line, 1), BDArmorySettings.RESOURCE_STEAL_RESPECT_FLOWSTATE_IN, Localizer.Format("#LOC_BDArmory_Settings_ResourceSteal_RespectFlowStateIn"));//Respect Flow State In - BDArmorySettings.RESOURCE_STEAL_RESPECT_FLOWSTATE_OUT = GUI.Toggle(SRightRect(line, 1), BDArmorySettings.RESOURCE_STEAL_RESPECT_FLOWSTATE_OUT, Localizer.Format("#LOC_BDArmory_Settings_ResourceSteal_RespectFlowStateOut"));//Respect Flow State Out - GUI.Label(SLeftSliderRect(++line, 1), $"{Localizer.Format("#LOC_BDArmory_Settings_FuelStealRation")}: ({BDArmorySettings.RESOURCE_STEAL_FUEL_RATION})", leftLabel);//Fuel Steal Ration + BDArmorySettings.RESOURCE_STEAL_RESPECT_FLOWSTATE_IN = GUI.Toggle(SLeftRect(++line, 1), BDArmorySettings.RESOURCE_STEAL_RESPECT_FLOWSTATE_IN, StringUtils.Localize("#LOC_BDArmory_Settings_ResourceSteal_RespectFlowStateIn"));//Respect Flow State In + BDArmorySettings.RESOURCE_STEAL_RESPECT_FLOWSTATE_OUT = GUI.Toggle(SRightRect(line, 1), BDArmorySettings.RESOURCE_STEAL_RESPECT_FLOWSTATE_OUT, StringUtils.Localize("#LOC_BDArmory_Settings_ResourceSteal_RespectFlowStateOut"));//Respect Flow State Out + GUI.Label(SLeftSliderRect(++line, 1), $"{StringUtils.Localize("#LOC_BDArmory_Settings_FuelStealRation")}: ({BDArmorySettings.RESOURCE_STEAL_FUEL_RATION})", leftLabel);//Fuel Steal Ration BDArmorySettings.RESOURCE_STEAL_FUEL_RATION = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.RESOURCE_STEAL_FUEL_RATION, 0f, 1f) * 100f) / 100f; - GUI.Label(SLeftSliderRect(++line, 1), $"{Localizer.Format("#LOC_BDArmory_Settings_AmmoStealRation")}: ({BDArmorySettings.RESOURCE_STEAL_AMMO_RATION})", leftLabel);//Ammo Steal Ration + GUI.Label(SLeftSliderRect(++line, 1), $"{StringUtils.Localize("#LOC_BDArmory_Settings_AmmoStealRation")}: ({BDArmorySettings.RESOURCE_STEAL_AMMO_RATION})", leftLabel);//Ammo Steal Ration BDArmorySettings.RESOURCE_STEAL_AMMO_RATION = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.RESOURCE_STEAL_AMMO_RATION, 0f, 1f) * 100f) / 100f; - GUI.Label(SLeftSliderRect(++line, 1), $"{Localizer.Format("#LOC_BDArmory_Settings_CMStealRation")}: ({BDArmorySettings.RESOURCE_STEAL_CM_RATION})", leftLabel);//CM Steal Ration + GUI.Label(SLeftSliderRect(++line, 1), $"{StringUtils.Localize("#LOC_BDArmory_Settings_CMStealRation")}: ({BDArmorySettings.RESOURCE_STEAL_CM_RATION})", leftLabel);//CM Steal Ration BDArmorySettings.RESOURCE_STEAL_CM_RATION = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.RESOURCE_STEAL_CM_RATION, 0f, 1f) * 100f) / 100f; } - var oldSpaceHacks = BDArmorySettings.SPACE_HACKS; - BDArmorySettings.SPACE_HACKS = GUI.Toggle(SLeftRect(++line), BDArmorySettings.SPACE_HACKS, Localizer.Format("#LOC_BDArmory_Settings_SpaceHacks")); + bool oldSpaceHacks = BDArmorySettings.SPACE_HACKS; + BDArmorySettings.SPACE_HACKS = GUI.Toggle(SLeftRect(++line), BDArmorySettings.SPACE_HACKS, StringUtils.Localize("#LOC_BDArmory_Settings_SpaceHacks"));//Space Tools + if (BDArmorySettings.SPACE_HACKS) { - if (BDArmorySettings.SPACE_HACKS) - { - if (!oldSpaceHacks) ModuleSpaceFriction.AddSpaceFrictionToAllValidVessels(); // Add missing modules when Space Hacks is toggled. - BDArmorySettings.SF_FRICTION = GUI.Toggle(SLeftRect(++line, 1f), BDArmorySettings.SF_FRICTION, Localizer.Format("#LOC_BDArmory_Settings_SpaceFriction")); - BDArmorySettings.SF_GRAVITY = GUI.Toggle(SLeftRect(++line, 1f), BDArmorySettings.SF_GRAVITY, Localizer.Format("#LOC_BDArmory_Settings_IgnoreGravity")); - GUI.Label(SLeftSliderRect(++line, 1f), $"{Localizer.Format("#LOC_BDArmory_Settings_SpaceFrictionMult")}: ({BDArmorySettings.SF_DRAGMULT})", leftLabel);//Space Friction Mult - BDArmorySettings.SF_DRAGMULT = Mathf.Round(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.SF_DRAGMULT, 1f, 10)); - BDArmorySettings.SF_REPULSOR = GUI.Toggle(SLeftRect(++line, 1f), BDArmorySettings.SF_REPULSOR, Localizer.Format("#LOC_BDArmory_Settings_Repulsor")); - } - else + if (HighLogic.LoadedSceneIsFlight) { - BDArmorySettings.SF_FRICTION = false; - BDArmorySettings.SF_GRAVITY = false; - BDArmorySettings.SF_REPULSOR = false; + if (oldSpaceHacks != BDArmorySettings.SPACE_HACKS) + { + SpawnUtils.SpaceFrictionOnNewVessels(BDArmorySettings.SPACE_HACKS); + if (BDArmorySettings.SPACE_HACKS) // Add the hack to all in-game intakes. + { + foreach (var vessel in FlightGlobals.Vessels) + { + if (vessel == null || !vessel.loaded) continue; + SpawnUtils.SpaceHacks(vessel); + } + } + } } + //ModuleSpaceFriction.AddSpaceFrictionToAllValidVessels(); // Add missing modules when Space Hacks is toggled. + + BDArmorySettings.SF_FRICTION = GUI.Toggle(SLeftRect(++line, 1f), BDArmorySettings.SF_FRICTION, StringUtils.Localize("#LOC_BDArmory_Settings_SpaceFriction")); + BDArmorySettings.SF_GRAVITY = GUI.Toggle(SLeftRect(++line, 1f), BDArmorySettings.SF_GRAVITY, StringUtils.Localize("#LOC_BDArmory_Settings_IgnoreGravity")); + GUI.Label(SLeftSliderRect(++line, 1f), $"{StringUtils.Localize("#LOC_BDArmory_Settings_SpaceFrictionMult")}: ({BDArmorySettings.SF_DRAGMULT})", leftLabel);//Space Friction Mult + BDArmorySettings.SF_DRAGMULT = Mathf.Round(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.SF_DRAGMULT, 0f, 50)); + BDArmorySettings.SF_REPULSOR = GUI.Toggle(SLeftRect(++line, 1f), BDArmorySettings.SF_REPULSOR, $"{StringUtils.Localize("#LOC_BDArmory_Settings_Repulsor")} ({BDArmorySettings.SF_REPULSOR_STRENGTH:0.0})"); + BDArmorySettings.SF_REPULSOR_STRENGTH = BDAMath.RoundToUnit(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.SF_REPULSOR_STRENGTH, 1f, 10f), 0.1f); + } + else + { + BDArmorySettings.SF_FRICTION = false; + BDArmorySettings.SF_GRAVITY = false; + BDArmorySettings.SF_REPULSOR = false; } + // Asteroids - if (BDArmorySettings.ASTEROID_FIELD != (BDArmorySettings.ASTEROID_FIELD = GUI.Toggle(SLeftRect(++line), BDArmorySettings.ASTEROID_FIELD, Localizer.Format("#LOC_BDArmory_Settings_AsteroidField")))) // Asteroid Field + if (BDArmorySettings.ASTEROID_FIELD != (BDArmorySettings.ASTEROID_FIELD = GUI.Toggle(SLeftRect(++line), BDArmorySettings.ASTEROID_FIELD, StringUtils.Localize("#LOC_BDArmory_Settings_AsteroidField")))) // Asteroid Field { if (!BDArmorySettings.ASTEROID_FIELD) AsteroidField.Instance.Reset(true); } @@ -2736,15 +3255,15 @@ void WindowSettings(int windowID) AsteroidField.Instance.SpawnField(BDArmorySettings.ASTEROID_FIELD_NUMBER, BDArmorySettings.ASTEROID_FIELD_ALTITUDE, BDArmorySettings.ASTEROID_FIELD_RADIUS, BDArmorySettings.VESSEL_SPAWN_GEOCOORDS); } line += 0.25f; - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_AsteroidFieldNumber")}: ({BDArmorySettings.ASTEROID_FIELD_NUMBER})", leftLabel); + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_AsteroidFieldNumber")}: ({BDArmorySettings.ASTEROID_FIELD_NUMBER})", leftLabel); BDArmorySettings.ASTEROID_FIELD_NUMBER = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), Mathf.Round(BDArmorySettings.ASTEROID_FIELD_NUMBER / 10f), 1f, 200f) * 10f); // Asteroid Field Number var altitudeString = BDArmorySettings.ASTEROID_FIELD_ALTITUDE < 10f ? $"{BDArmorySettings.ASTEROID_FIELD_ALTITUDE * 100f:F0}m" : $"{BDArmorySettings.ASTEROID_FIELD_ALTITUDE / 10f:F1}km"; - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_AsteroidFieldAltitude")}: ({altitudeString})", leftLabel); + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_AsteroidFieldAltitude")}: ({altitudeString})", leftLabel); BDArmorySettings.ASTEROID_FIELD_ALTITUDE = Mathf.Round(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.ASTEROID_FIELD_ALTITUDE, 1f, 200f)); // Asteroid Field Altitude - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_AsteroidFieldRadius")}: ({BDArmorySettings.ASTEROID_FIELD_RADIUS}km)", leftLabel); + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_AsteroidFieldRadius")}: ({BDArmorySettings.ASTEROID_FIELD_RADIUS}km)", leftLabel); BDArmorySettings.ASTEROID_FIELD_RADIUS = Mathf.Round(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.ASTEROID_FIELD_RADIUS, 1f, 10f)); // Asteroid Field Radius line -= 0.25f; - if (BDArmorySettings.ASTEROID_FIELD_ANOMALOUS_ATTRACTION != (BDArmorySettings.ASTEROID_FIELD_ANOMALOUS_ATTRACTION = GUI.Toggle(SLeftRect(++line), BDArmorySettings.ASTEROID_FIELD_ANOMALOUS_ATTRACTION, BDArmorySettings.ASTEROID_FIELD_ANOMALOUS_ATTRACTION ? $"{Localizer.Format("#LOC_BDArmory_Settings_AsteroidFieldAnomalousAttraction")}: ({BDArmorySettings.ASTEROID_FIELD_ANOMALOUS_ATTRACTION_STRENGTH:G2})" : Localizer.Format("#LOC_BDArmory_Settings_AsteroidFieldAnomalousAttraction")))) // Anomalous Attraction + if (BDArmorySettings.ASTEROID_FIELD_ANOMALOUS_ATTRACTION != (BDArmorySettings.ASTEROID_FIELD_ANOMALOUS_ATTRACTION = GUI.Toggle(SLeftRect(++line), BDArmorySettings.ASTEROID_FIELD_ANOMALOUS_ATTRACTION, BDArmorySettings.ASTEROID_FIELD_ANOMALOUS_ATTRACTION ? $"{StringUtils.Localize("#LOC_BDArmory_Settings_AsteroidFieldAnomalousAttraction")}: ({BDArmorySettings.ASTEROID_FIELD_ANOMALOUS_ATTRACTION_STRENGTH:G2})" : StringUtils.Localize("#LOC_BDArmory_Settings_AsteroidFieldAnomalousAttraction")))) // Anomalous Attraction { if (!BDArmorySettings.ASTEROID_FIELD_ANOMALOUS_ATTRACTION && AsteroidField.Instance != null) { AsteroidField.Instance.anomalousAttraction = Vector3d.zero; } @@ -2754,7 +3273,7 @@ void WindowSettings(int windowID) BDArmorySettings.ASTEROID_FIELD_ANOMALOUS_ATTRACTION_STRENGTH = Mathf.Round(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.ASTEROID_FIELD_ANOMALOUS_ATTRACTION_STRENGTH * 20f, 1f, 20f)) / 20f; // Asteroid Field Anomalous Attraction Strength } } - if (BDArmorySettings.ASTEROID_RAIN != (BDArmorySettings.ASTEROID_RAIN = GUI.Toggle(SLeftRect(++line), BDArmorySettings.ASTEROID_RAIN, Localizer.Format("#LOC_BDArmory_Settings_AsteroidRain")))) // Asteroid Rain + if (BDArmorySettings.ASTEROID_RAIN != (BDArmorySettings.ASTEROID_RAIN = GUI.Toggle(SLeftRect(++line), BDArmorySettings.ASTEROID_RAIN, StringUtils.Localize("#LOC_BDArmory_Settings_AsteroidRain")))) // Asteroid Rain { if (!BDArmorySettings.ASTEROID_RAIN) AsteroidRain.Instance.Reset(); } @@ -2769,50 +3288,73 @@ void WindowSettings(int windowID) else AsteroidRain.Instance.SpawnRain(BDArmorySettings.VESSEL_SPAWN_GEOCOORDS); } - BDArmorySettings.ASTEROID_RAIN_FOLLOWS_CENTROID = GUI.Toggle(SLeftRect(++line), BDArmorySettings.ASTEROID_RAIN_FOLLOWS_CENTROID, Localizer.Format("#LOC_BDArmory_Settings_AsteroidRainFollowsCentroid")); // Follows Vessels' Location. + BDArmorySettings.ASTEROID_RAIN_FOLLOWS_CENTROID = GUI.Toggle(SLeftRect(++line), BDArmorySettings.ASTEROID_RAIN_FOLLOWS_CENTROID, StringUtils.Localize("#LOC_BDArmory_Settings_AsteroidRainFollowsCentroid")); // Follows Vessels' Location. if (BDArmorySettings.ASTEROID_RAIN_FOLLOWS_CENTROID) { - BDArmorySettings.ASTEROID_RAIN_FOLLOWS_SPREAD = GUI.Toggle(SRightRect(line), BDArmorySettings.ASTEROID_RAIN_FOLLOWS_SPREAD, Localizer.Format("#LOC_BDArmory_Settings_AsteroidRainFollowsSpread")); // Follows Vessels' Spread. + BDArmorySettings.ASTEROID_RAIN_FOLLOWS_SPREAD = GUI.Toggle(SRightRect(line), BDArmorySettings.ASTEROID_RAIN_FOLLOWS_SPREAD, StringUtils.Localize("#LOC_BDArmory_Settings_AsteroidRainFollowsSpread")); // Follows Vessels' Spread. } line += 0.25f; - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_AsteroidRainNumber")}: ({BDArmorySettings.ASTEROID_RAIN_NUMBER})", leftLabel); + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_AsteroidRainNumber")}: ({BDArmorySettings.ASTEROID_RAIN_NUMBER})", leftLabel); if (BDArmorySettings.ASTEROID_RAIN_NUMBER != (BDArmorySettings.ASTEROID_RAIN_NUMBER = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), Mathf.Round(BDArmorySettings.ASTEROID_RAIN_NUMBER / 10f), 1f, 200f) * 10f))) // Asteroid Rain Number { if (HighLogic.LoadedSceneIsFlight) AsteroidRain.Instance.UpdateSettings(); } var altitudeString = BDArmorySettings.ASTEROID_RAIN_ALTITUDE < 10f ? $"{BDArmorySettings.ASTEROID_RAIN_ALTITUDE * 100f:F0}m" : $"{BDArmorySettings.ASTEROID_RAIN_ALTITUDE / 10f:F1}km"; - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_AsteroidRainAltitude")}: ({altitudeString})", leftLabel); + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_AsteroidRainAltitude")}: ({altitudeString})", leftLabel); if (BDArmorySettings.ASTEROID_RAIN_ALTITUDE != (BDArmorySettings.ASTEROID_RAIN_ALTITUDE = Mathf.Round(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.ASTEROID_RAIN_ALTITUDE, 1f, 100f)))) // Asteroid Rain Altitude { if (HighLogic.LoadedSceneIsFlight) AsteroidRain.Instance.UpdateSettings(); } if (!BDArmorySettings.ASTEROID_RAIN_FOLLOWS_SPREAD) { - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_AsteroidRainRadius")}: ({BDArmorySettings.ASTEROID_RAIN_RADIUS}km)", leftLabel); + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_AsteroidRainRadius")}: ({BDArmorySettings.ASTEROID_RAIN_RADIUS}km)", leftLabel); if (BDArmorySettings.ASTEROID_RAIN_RADIUS != (BDArmorySettings.ASTEROID_RAIN_RADIUS = Mathf.Round(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.ASTEROID_RAIN_RADIUS, 1f, 10f)))) // Asteroid Rain Radius { if (HighLogic.LoadedSceneIsFlight) AsteroidRain.Instance.UpdateSettings(); } } line -= 0.25f; } - BDArmorySettings.WAYPOINTS_MODE = GUI.Toggle(SLeftRect(++line), BDArmorySettings.WAYPOINTS_MODE, Localizer.Format("#LOC_BDArmory_Settings_WaypointsMode")); - if (BDArmorySettings.ADVANDED_USER_SETTINGS) + BDArmorySettings.WAYPOINTS_MODE = GUI.Toggle(SLeftRect(++line), BDArmorySettings.WAYPOINTS_MODE, StringUtils.Localize("#LOC_BDArmory_Settings_WaypointsMode")); + if (BDArmorySettings.ADVANCED_USER_SETTINGS) { - BDArmorySettings.RUNWAY_PROJECT = GUI.Toggle(SLeftRect(++line), BDArmorySettings.RUNWAY_PROJECT, Localizer.Format("#LOC_BDArmory_Settings_RunwayProject"));//Runway Project + if (BDArmorySettings.RUNWAY_PROJECT != (BDArmorySettings.RUNWAY_PROJECT = GUI.Toggle(SLeftRect(++line), BDArmorySettings.RUNWAY_PROJECT, StringUtils.Localize("#LOC_BDArmory_Settings_RunwayProject"))))//Runway Project + { + if (HighLogic.LoadedSceneIsFlight) + { + SpawnUtils.HackActuatorsOnNewVessels(BDArmorySettings.RUNWAY_PROJECT); + foreach (var vessel in FlightGlobals.Vessels) + { + if (vessel == null || !vessel.loaded) continue; + SpawnUtils.HackActuators(vessel, BDArmorySettings.RUNWAY_PROJECT); + } + } + if (HighLogic.LoadedSceneIsEditor && EditorLogic.fetch.ship is not null) GameEvents.onEditorShipModified.Fire(EditorLogic.fetch.ship); + } if (BDArmorySettings.RUNWAY_PROJECT) { - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_RunwayProjectRound")}: ({(BDArmorySettings.RUNWAY_PROJECT_ROUND > 10 ? $"S{(BDArmorySettings.RUNWAY_PROJECT_ROUND - 1) / 10}R{(BDArmorySettings.RUNWAY_PROJECT_ROUND - 1) % 10 + 1}" : "—")})", leftLabel); // RWP round + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_RunwayProjectRound")}: ({(BDArmorySettings.RUNWAY_PROJECT_ROUND > 10 ? $"S{(BDArmorySettings.RUNWAY_PROJECT_ROUND - 1) / 10}R{(BDArmorySettings.RUNWAY_PROJECT_ROUND - 1) % 10 + 1}" : "—")})", leftLabel); // RWP round BDArmorySettings.RUNWAY_PROJECT_ROUND = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.RUNWAY_PROJECT_ROUND, 10f, 60f)); if (BDArmorySettings.RUNWAY_PROJECT_ROUND == 41) { - GUI.Label(SLeftSliderRect(++line, 1f), $"{Localizer.Format("#LOC_BDArmory_settings_FireRateCenter")}: ({BDArmorySettings.FIRE_RATE_OVERRIDE_CENTER})", leftLabel);//Fire Rate Override Center + GUI.Label(SLeftSliderRect(++line, 1f), $"{StringUtils.Localize("#LOC_BDArmory_settings_FireRateCenter")}: ({BDArmorySettings.FIRE_RATE_OVERRIDE_CENTER})", leftLabel);//Fire Rate Override Center BDArmorySettings.FIRE_RATE_OVERRIDE_CENTER = Mathf.Round(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.FIRE_RATE_OVERRIDE_CENTER, 10f, 300f) / 5f) * 5f; - GUI.Label(SLeftSliderRect(++line, 1f), $"{Localizer.Format("#LOC_BDArmory_settings_FireRateSpread")}: ({BDArmorySettings.FIRE_RATE_OVERRIDE_SPREAD})", leftLabel);//Fire Rate Override Spread + GUI.Label(SLeftSliderRect(++line, 1f), $"{StringUtils.Localize("#LOC_BDArmory_settings_FireRateSpread")}: ({BDArmorySettings.FIRE_RATE_OVERRIDE_SPREAD})", leftLabel);//Fire Rate Override Spread BDArmorySettings.FIRE_RATE_OVERRIDE_SPREAD = Mathf.Round(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.FIRE_RATE_OVERRIDE_SPREAD, 0f, 50f)); - GUI.Label(SLeftSliderRect(++line, 1f), $"{Localizer.Format("#LOC_BDArmory_settings_FireRateBias")}: ({BDArmorySettings.FIRE_RATE_OVERRIDE_BIAS * BDArmorySettings.FIRE_RATE_OVERRIDE_BIAS:G2})", leftLabel);//Fire Rate Override Bias + GUI.Label(SLeftSliderRect(++line, 1f), $"{StringUtils.Localize("#LOC_BDArmory_settings_FireRateBias")}: ({BDArmorySettings.FIRE_RATE_OVERRIDE_BIAS * BDArmorySettings.FIRE_RATE_OVERRIDE_BIAS:G2})", leftLabel);//Fire Rate Override Bias BDArmorySettings.FIRE_RATE_OVERRIDE_BIAS = Mathf.Round(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.FIRE_RATE_OVERRIDE_BIAS, 0f, 1f) * 50f) / 50f; - GUI.Label(SLeftSliderRect(++line, 1f), $"{Localizer.Format("#LOC_BDArmory_settings_FireRateHitMultiplier")}: ({BDArmorySettings.FIRE_RATE_OVERRIDE_HIT_MULTIPLIER})", leftLabel);//Fire Rate Hit Multiplier + GUI.Label(SLeftSliderRect(++line, 1f), $"{StringUtils.Localize("#LOC_BDArmory_settings_FireRateHitMultiplier")}: ({BDArmorySettings.FIRE_RATE_OVERRIDE_HIT_MULTIPLIER})", leftLabel);//Fire Rate Hit Multiplier BDArmorySettings.FIRE_RATE_OVERRIDE_HIT_MULTIPLIER = Mathf.Round(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.FIRE_RATE_OVERRIDE_HIT_MULTIPLIER, 1f, 4f) * 10f) / 10f; } + if (BDArmorySettings.RUNWAY_PROJECT_ROUND == 55 && !BDArmorySettings.WAYPOINTS_MODE) + { + BDArmorySettings.WAYPOINTS_MODE = true; + } + if (BDArmorySettings.RUNWAY_PROJECT_ROUND == 60 && !BDArmorySettings.SPACE_HACKS) + { + BDArmorySettings.SPACE_HACKS = true; + BDArmorySettings.SF_FRICTION = true; + BDArmorySettings.SF_GRAVITY = true; + BDArmorySettings.SF_DRAGMULT = 30; + } // if (BDArmorySettings.RUNWAY_PROJECT_ROUND == 46) BDArmorySettings.NO_ENGINES = true; - if (CheatCodeGUI != (CheatCodeGUI = GUI.TextField(SLeftRect(++line, 1, true), CheatCodeGUI))) //if we need super-secret stuff + if (CheatCodeGUI != (CheatCodeGUI = GUI.TextField(SLeftRect(++line, 1, true), CheatCodeGUI, inputFieldStyle))) //if we need super-secret stuff { if (CheatCodeGUI == "ZombieMode") { @@ -2834,32 +3376,37 @@ void WindowSettings(int windowID) BDArmorySettings.ENABLE_HOS = !BDArmorySettings.ENABLE_HOS; CheatCodeGUI = ""; } + else if (CheatCodeGUI.ToLower() == "altitudehack") //until we figure out where to put this + { + BDArmorySettings.ALTITUDE_HACKS = !BDArmorySettings.ALTITUDE_HACKS; + CheatCodeGUI = ""; + } } - //BDArmorySettings.ZOMBIE_MODE = GUI.Toggle(SLeftRect(++line), BDArmorySettings.ZOMBIE_MODE, Localizer.Format("#LOC_BDArmory_settings_ZombieMode")); + //BDArmorySettings.ZOMBIE_MODE = GUI.Toggle(SLeftRect(++line), BDArmorySettings.ZOMBIE_MODE, StringUtils.Localize("#LOC_BDArmory_settings_ZombieMode")); if (BDArmorySettings.ZOMBIE_MODE) { - GUI.Label(SLeftSliderRect(++line, 1f), $"{Localizer.Format("#LOC_BDArmory_settings_zombieDmgMod")}: ({BDArmorySettings.ZOMBIE_DMG_MULT})", leftLabel);//"S4R2 Non-headshot Dmg Mult" + GUI.Label(SLeftSliderRect(++line, 1f), $"{StringUtils.Localize("#LOC_BDArmory_settings_zombieDmgMod")}: ({BDArmorySettings.ZOMBIE_DMG_MULT})", leftLabel);//"S4R2 Non-headshot Dmg Mult" //if (BDArmorySettings.RUNWAY_PROJECT_ROUND == -1) // FIXME Set when the round is actually run! Also check for other "RUNWAY_PROJECT_ROUND == -1" checks. //{ - // GUI.Label(SLeftSliderRect(++line, 1f), $"{Localizer.Format("#LOC_BDArmory_settings_zombieDmgMod")}: ({BDArmorySettings.ZOMBIE_DMG_MULT})", leftLabel);//"Zombie Non-headshot Dmg Mult" + // GUI.Label(SLeftSliderRect(++line, 1f), $"{StringUtils.Localize("#LOC_BDArmory_settings_zombieDmgMod")}: ({BDArmorySettings.ZOMBIE_DMG_MULT})", leftLabel);//"Zombie Non-headshot Dmg Mult" BDArmorySettings.ZOMBIE_DMG_MULT = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.ZOMBIE_DMG_MULT, 0.05f, 0.95f) * 100f) / 100f; if (BDArmorySettings.BATTLEDAMAGE) { - BDArmorySettings.ALLOW_ZOMBIE_BD = GUI.Toggle(SLeftRect(++line, 1), BDArmorySettings.ALLOW_ZOMBIE_BD, Localizer.Format("#LOC_BDArmory_Settings_BD_ZombieMode"));//"Allow battle Damage" + BDArmorySettings.ALLOW_ZOMBIE_BD = GUI.Toggle(SLeftRect(++line, 1), BDArmorySettings.ALLOW_ZOMBIE_BD, StringUtils.Localize("#LOC_BDArmory_Settings_BD_ZombieMode"));//"Allow battle Damage" } } if (BDArmorySettings.ENABLE_HOS) { - GUI.Label(SLeftRect(++line), Localizer.Format("--Hall Of Shame Enabled--"));//"Competition Distance" - HoSString = GUI.TextField(SLeftRect(++line, 1, true), HoSString); + GUI.Label(SLeftRect(++line), StringUtils.Localize("--Hall Of Shame Enabled--"));//"Competition Distance" + HoSString = GUI.TextField(SLeftRect(++line, 1, true), HoSString, inputFieldStyle); if (!string.IsNullOrEmpty(HoSString)) { - enteredHoS = GUI.Toggle(SRightRect(line), enteredHoS, Localizer.Format("Enter to Hall of Shame")); + enteredHoS = GUI.Toggle(SRightRect(line), enteredHoS, StringUtils.Localize("Enter to Hall of Shame")); { if (enteredHoS) - { + { if (HoSString == "Clear()") { BDArmorySettings.HALL_OF_SHAME_LIST.Clear(); @@ -2880,17 +3427,18 @@ void WindowSettings(int windowID) } } } - GUI.Label(SLeftRect(++line), Localizer.Format("--Select Punishment--")); - GUI.Label(SLeftSliderRect(++line, 2f), $"{Localizer.Format("Fire")}: ({(float)Math.Round(BDArmorySettings.HOS_FIRE, 1)} Burn Rate)", leftLabel); + GUI.Label(SLeftRect(++line), StringUtils.Localize("--Select Punishment--")); + GUI.Label(SLeftSliderRect(++line, 2f), $"{StringUtils.Localize("Fire")}: ({(float)Math.Round(BDArmorySettings.HOS_FIRE, 1)} Burn Rate)", leftLabel); BDArmorySettings.HOS_FIRE = (GUI.HorizontalSlider(SRightSliderRect(line), (float)Math.Round(BDArmorySettings.HOS_FIRE, 1), 0, 10)); - GUI.Label(SLeftSliderRect(++line, 2f), $"{Localizer.Format("Mass")}: ({(float)Math.Round(BDArmorySettings.HOS_MASS, 1)} ton deadweight)", leftLabel); + GUI.Label(SLeftSliderRect(++line, 2f), $"{StringUtils.Localize("Mass")}: ({(float)Math.Round(BDArmorySettings.HOS_MASS, 1)} ton deadweight)", leftLabel); BDArmorySettings.HOS_MASS = (GUI.HorizontalSlider(SRightSliderRect(line), (float)Math.Round(BDArmorySettings.HOS_MASS, 1), -10, 10)); - GUI.Label(SLeftSliderRect(++line, 2f), $"{Localizer.Format("Frailty")}: ({(float)Math.Round(BDArmorySettings.HOS_DMG, 2) * 100}%) Dmg taken", leftLabel); + GUI.Label(SLeftSliderRect(++line, 2f), $"{StringUtils.Localize("Frailty")}: ({(float)Math.Round(BDArmorySettings.HOS_DMG, 2) * 100}%) Dmg taken", leftLabel); BDArmorySettings.HOS_DMG = (GUI.HorizontalSlider(SRightSliderRect(line), (float)Math.Round(BDArmorySettings.HOS_DMG, 2), 0.1f, 10)); - GUI.Label(SLeftSliderRect(++line, 2f), $"{Localizer.Format("Thrust")}: ({(float)Math.Round(BDArmorySettings.HOS_THRUST, 1)}%) Engine Thrust", leftLabel); + GUI.Label(SLeftSliderRect(++line, 2f), $"{StringUtils.Localize("Thrust")}: ({(float)Math.Round(BDArmorySettings.HOS_THRUST, 1)}%) Engine Thrust", leftLabel); BDArmorySettings.HOS_THRUST = (GUI.HorizontalSlider(SRightSliderRect(line), (float)Math.Round(BDArmorySettings.HOS_THRUST, 1), 0, 200)); - GUI.Label(SLeftRect(++line), Localizer.Format("--Shame badge--")); - HoSTag = GUI.TextField(SLeftRect(++line, 1, true), HoSTag); + BDArmorySettings.HOS_SAS = GUI.Toggle(SLeftRect(++line), BDArmorySettings.HOS_SAS, "Remove Reaction Wheels"); + GUI.Label(SLeftRect(++line), StringUtils.Localize("--Shame badge--")); + HoSTag = GUI.TextField(SLeftRect(++line, 1, true), HoSTag, inputFieldStyle); BDArmorySettings.HOS_BADGE = HoSTag; } else @@ -2899,7 +3447,8 @@ void WindowSettings(int windowID) BDArmorySettings.HOS_MASS = 0; BDArmorySettings.HOS_DMG = 100; BDArmorySettings.HOS_THRUST = 100; - //partloss = false; //- would need special module, but could also be a mutator mode + BDArmorySettings.HOS_SAS = false; + //partloss = false; //- would need special module, but could also be a mutator mode //timebomb = false //same //might be more elegant to simply have this use Mutator framework and load the HoS craft with a select mutator(s) instead... Something to look into later, maybe, but ideally this shouldn't need to be used in the first place. } @@ -2911,7 +3460,7 @@ void WindowSettings(int windowID) if (BDArmorySettings.BATTLEDAMAGE) { - if (GUI.Button(SLineRect(++line), (BDArmorySettings.BATTLEDAMAGE_TOGGLE ? "Hide " : "Show ") + Localizer.Format("#LOC_BDArmory_Settings_BDSettingsToggle")))//Show/hide Battle Damage settings. + if (GUI.Button(SLineRect(++line), $"{(BDArmorySettings.BATTLEDAMAGE_TOGGLE ? StringUtils.Localize("#LOC_BDArmory_Generic_Hide") : StringUtils.Localize("#LOC_BDArmory_Generic_Show"))} {StringUtils.Localize("#LOC_BDArmory_Settings_BDSettingsToggle")}"))//Show/hide Battle Damage settings. { BDArmorySettings.BATTLEDAMAGE_TOGGLE = !BDArmorySettings.BATTLEDAMAGE_TOGGLE; } @@ -2919,67 +3468,69 @@ void WindowSettings(int windowID) { line += 0.2f; - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_BD_Proc")}: ({BDArmorySettings.BD_DAMAGE_CHANCE}%)", leftLabel); //Proc Chance Frequency + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_BD_Proc")}: ({BDArmorySettings.BD_DAMAGE_CHANCE}%)", leftLabel); //Proc Chance Frequency BDArmorySettings.BD_DAMAGE_CHANCE = Mathf.Round(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.BD_DAMAGE_CHANCE, 0f, 100)); - BDArmorySettings.BD_PROPULSION = GUI.Toggle(SLeftRect(++line), BDArmorySettings.BD_PROPULSION, Localizer.Format("#LOC_BDArmory_Settings_BD_Engines"));//"Propulsion Systems Damage" - if (BDArmorySettings.BD_PROPULSION && BDArmorySettings.ADVANDED_USER_SETTINGS) + BDArmorySettings.BD_PROPULSION = GUI.Toggle(SLeftRect(++line), BDArmorySettings.BD_PROPULSION, StringUtils.Localize("#LOC_BDArmory_Settings_BD_Engines"));//"Propulsion Systems Damage" + if (BDArmorySettings.BD_PROPULSION && BDArmorySettings.ADVANCED_USER_SETTINGS) { - GUI.Label(SLeftSliderRect(++line, 1f), $"{Localizer.Format("#LOC_BDArmory_Settings_BD_Prop_Dmg_Mult")}: ({BDArmorySettings.BD_PROP_DAM_RATE}x)", leftLabel); //Propulsion Damage Multiplier + GUI.Label(SLeftSliderRect(++line, 1f), $"{StringUtils.Localize("#LOC_BDArmory_Settings_BD_Prop_Dmg_Mult")}: ({BDArmorySettings.BD_PROP_DAM_RATE}x)", leftLabel); //Propulsion Damage Multiplier BDArmorySettings.BD_PROP_DAM_RATE = (GUI.HorizontalSlider(SRightSliderRect(line), (float)Math.Round(BDArmorySettings.BD_PROP_DAM_RATE, 1), 0, 2)); - GUI.Label(SLeftSliderRect(++line, 1f), $"{Localizer.Format("#LOC_BDArmory_Settings_BD_Prop_floor")}: ({BDArmorySettings.BD_PROP_FLOOR}%)", leftLabel); //Min Engine Thrust + GUI.Label(SLeftSliderRect(++line, 1f), $"{StringUtils.Localize("#LOC_BDArmory_Settings_BD_Prop_floor")}: ({BDArmorySettings.BD_PROP_FLOOR}%)", leftLabel); //Min Engine Thrust BDArmorySettings.BD_PROP_FLOOR = (GUI.HorizontalSlider(SRightSliderRect(line), (float)Math.Round(BDArmorySettings.BD_PROP_FLOOR, 1), 0, 100)); - GUI.Label(SLeftSliderRect(++line, 1f), $"{Localizer.Format("#LOC_BDArmory_Settings_BD_Prop_flameout")}: ({BDArmorySettings.BD_PROP_FLAMEOUT}% HP)", leftLabel); //Engine Flameout + GUI.Label(SLeftSliderRect(++line, 1f), $"{StringUtils.Localize("#LOC_BDArmory_Settings_BD_Prop_flameout")}: ({BDArmorySettings.BD_PROP_FLAMEOUT}% HP)", leftLabel); //Engine Flameout BDArmorySettings.BD_PROP_FLAMEOUT = (GUI.HorizontalSlider(SRightSliderRect(line), (float)Math.Round(BDArmorySettings.BD_PROP_FLAMEOUT, 0), 0, 95)); - BDArmorySettings.BD_INTAKES = GUI.Toggle(SLeftRect(++line, 1f), BDArmorySettings.BD_INTAKES, Localizer.Format("#LOC_BDArmory_Settings_BD_Intakes"));//"Intake Damage" - BDArmorySettings.BD_GIMBALS = GUI.Toggle(SRightRect(line, 1f), BDArmorySettings.BD_GIMBALS, Localizer.Format("#LOC_BDArmory_Settings_BD_Gimbals"));//"Gimbal Damage" + BDArmorySettings.BD_INTAKES = GUI.Toggle(SLeftRect(++line, 1f), BDArmorySettings.BD_INTAKES, StringUtils.Localize("#LOC_BDArmory_Settings_BD_Intakes"));//"Intake Damage" + BDArmorySettings.BD_GIMBALS = GUI.Toggle(SRightRect(line, 1f), BDArmorySettings.BD_GIMBALS, StringUtils.Localize("#LOC_BDArmory_Settings_BD_Gimbals"));//"Gimbal Damage" } - BDArmorySettings.BD_AEROPARTS = GUI.Toggle(SLeftRect(++line), BDArmorySettings.BD_AEROPARTS, Localizer.Format("#LOC_BDArmory_Settings_BD_Aero"));//"Flight Systems Damage" - if (BDArmorySettings.BD_AEROPARTS && BDArmorySettings.ADVANDED_USER_SETTINGS) + BDArmorySettings.BD_AEROPARTS = GUI.Toggle(SLeftRect(++line), BDArmorySettings.BD_AEROPARTS, StringUtils.Localize("#LOC_BDArmory_Settings_BD_Aero"));//"Flight Systems Damage" + if (BDArmorySettings.BD_AEROPARTS && BDArmorySettings.ADVANCED_USER_SETTINGS) { - GUI.Label(SLeftSliderRect(++line, 1f), $"{Localizer.Format("#LOC_BDArmory_Settings_BD_Aero_Dmg_Mult")}: ({BDArmorySettings.BD_LIFT_LOSS_RATE}x)", leftLabel); //Wing Damage Magnitude + GUI.Label(SLeftSliderRect(++line, 1f), $"{StringUtils.Localize("#LOC_BDArmory_Settings_BD_Aero_Dmg_Mult")}: ({BDArmorySettings.BD_LIFT_LOSS_RATE}x)", leftLabel); //Wing Damage Magnitude BDArmorySettings.BD_LIFT_LOSS_RATE = (GUI.HorizontalSlider(SRightSliderRect(line), (float)Math.Round(BDArmorySettings.BD_LIFT_LOSS_RATE, 1), 0, 5)); - BDArmorySettings.BD_CTRL_SRF = GUI.Toggle(SLeftRect(++line, 1f), BDArmorySettings.BD_CTRL_SRF, Localizer.Format("#LOC_BDArmory_Settings_BD_CtrlSrf"));//"Ctrl Surface Damage" + BDArmorySettings.BD_CTRL_SRF = GUI.Toggle(SLeftRect(++line, 1f), BDArmorySettings.BD_CTRL_SRF, StringUtils.Localize("#LOC_BDArmory_Settings_BD_CtrlSrf"));//"Ctrl Surface Damage" } - BDArmorySettings.BD_COCKPITS = GUI.Toggle(SLeftRect(++line), BDArmorySettings.BD_COCKPITS, Localizer.Format("#LOC_BDArmory_Settings_BD_Command"));//"Command & Control Damage" - if (BDArmorySettings.BD_COCKPITS && BDArmorySettings.ADVANDED_USER_SETTINGS) + BDArmorySettings.BD_COCKPITS = GUI.Toggle(SLeftRect(++line), BDArmorySettings.BD_COCKPITS, StringUtils.Localize("#LOC_BDArmory_Settings_BD_Command"));//"Command & Control Damage" + if (BDArmorySettings.BD_COCKPITS && BDArmorySettings.ADVANCED_USER_SETTINGS) { - BDArmorySettings.BD_PILOT_KILLS = GUI.Toggle(SLeftRect(++line, 1f), BDArmorySettings.BD_PILOT_KILLS, Localizer.Format("#LOC_BDArmory_Settings_BD_PilotKill"));//"Crew Fatalities" + BDArmorySettings.BD_PILOT_KILLS = GUI.Toggle(SLeftRect(++line, 1f), BDArmorySettings.BD_PILOT_KILLS, StringUtils.Localize("#LOC_BDArmory_Settings_BD_PilotKill"));//"Crew Fatalities" } - BDArmorySettings.BD_TANKS = GUI.Toggle(SLeftRect(++line), BDArmorySettings.BD_TANKS, Localizer.Format("#LOC_BDArmory_Settings_BD_Tanks"));//"FuelTank Damage" - if (BDArmorySettings.BD_TANKS && BDArmorySettings.ADVANDED_USER_SETTINGS) + BDArmorySettings.BD_TANKS = GUI.Toggle(SLeftRect(++line), BDArmorySettings.BD_TANKS, StringUtils.Localize("#LOC_BDArmory_Settings_BD_Tanks"));//"FuelTank Damage" + if (BDArmorySettings.BD_TANKS && BDArmorySettings.ADVANCED_USER_SETTINGS) { - GUI.Label(SLeftSliderRect(++line, 1f), $"{Localizer.Format("#LOC_BDArmory_Settings_BD_Leak_Time")}: ({BDArmorySettings.BD_TANK_LEAK_TIME}s)", leftLabel); // Leak Duration + GUI.Label(SLeftSliderRect(++line, 1f), $"{StringUtils.Localize("#LOC_BDArmory_Settings_BD_Leak_Time")}: ({BDArmorySettings.BD_TANK_LEAK_TIME}s)", leftLabel); // Leak Duration BDArmorySettings.BD_TANK_LEAK_TIME = Mathf.Round((GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.BD_TANK_LEAK_TIME, 0, 100))); - GUI.Label(SLeftSliderRect(++line, 1f), $"{Localizer.Format("#LOC_BDArmory_Settings_BD_Leak_Rate")}: ({BDArmorySettings.BD_TANK_LEAK_RATE}x)", leftLabel); //Leak magnitude + GUI.Label(SLeftSliderRect(++line, 1f), $"{StringUtils.Localize("#LOC_BDArmory_Settings_BD_Leak_Rate")}: ({BDArmorySettings.BD_TANK_LEAK_RATE}x)", leftLabel); //Leak magnitude BDArmorySettings.BD_TANK_LEAK_RATE = (GUI.HorizontalSlider(SRightSliderRect(line), (float)Math.Round(BDArmorySettings.BD_TANK_LEAK_RATE, 1), 0, 5)); } - BDArmorySettings.BD_SUBSYSTEMS = GUI.Toggle(SLeftRect(++line), BDArmorySettings.BD_SUBSYSTEMS, Localizer.Format("#LOC_BDArmory_Settings_BD_SubSystems"));//"Subsystem Damage" - BDArmorySettings.BD_AMMOBINS = GUI.Toggle(SLeftRect(++line), BDArmorySettings.BD_AMMOBINS, Localizer.Format("#LOC_BDArmory_Settings_BD_Ammo"));//"Ammo Explosions" - if (BDArmorySettings.BD_AMMOBINS && BDArmorySettings.ADVANDED_USER_SETTINGS) + BDArmorySettings.BD_SUBSYSTEMS = GUI.Toggle(SLeftRect(++line), BDArmorySettings.BD_SUBSYSTEMS, StringUtils.Localize("#LOC_BDArmory_Settings_BD_SubSystems"));//"Subsystem Damage" + BDArmorySettings.BD_PART_STRENGTH = GUI.Toggle(SLeftRect(++line), BDArmorySettings.BD_PART_STRENGTH, StringUtils.Localize("#LOC_BDArmory_Settings_BD_JointStrength"));//"Structural Damage" + + BDArmorySettings.BD_AMMOBINS = GUI.Toggle(SLeftRect(++line), BDArmorySettings.BD_AMMOBINS, StringUtils.Localize("#LOC_BDArmory_Settings_BD_Ammo"));//"Ammo Explosions" + if (BDArmorySettings.BD_AMMOBINS && BDArmorySettings.ADVANCED_USER_SETTINGS) { - BDArmorySettings.BD_VOLATILE_AMMO = GUI.Toggle(SLineRect(++line, 1f), BDArmorySettings.BD_VOLATILE_AMMO, Localizer.Format("#LOC_BDArmory_Settings_BD_Volatile_Ammo"));//"Ammo Bins Explode When Destroyed" + BDArmorySettings.BD_VOLATILE_AMMO = GUI.Toggle(SLineRect(++line, 1f), BDArmorySettings.BD_VOLATILE_AMMO, StringUtils.Localize("#LOC_BDArmory_Settings_BD_Volatile_Ammo"));//"Ammo Bins Explode When Destroyed" } - BDArmorySettings.BD_FIRES_ENABLED = GUI.Toggle(SLeftRect(++line), BDArmorySettings.BD_FIRES_ENABLED, Localizer.Format("#LOC_BDArmory_Settings_BD_Fires"));//"Fires" - if (BDArmorySettings.BD_FIRES_ENABLED && BDArmorySettings.ADVANDED_USER_SETTINGS) + BDArmorySettings.BD_FIRES_ENABLED = GUI.Toggle(SLeftRect(++line), BDArmorySettings.BD_FIRES_ENABLED, StringUtils.Localize("#LOC_BDArmory_Settings_BD_Fires"));//"Fires" + if (BDArmorySettings.BD_FIRES_ENABLED && BDArmorySettings.ADVANCED_USER_SETTINGS) { - BDArmorySettings.BD_FIRE_DOT = GUI.Toggle(SLeftRect(++line, 1f), BDArmorySettings.BD_FIRE_DOT, Localizer.Format("#LOC_BDArmory_Settings_BD_DoT"));//"Fire Damage" - GUI.Label(SLeftSliderRect(++line, 1f), $"{Localizer.Format("#LOC_BDArmory_Settings_BD_Fire_Dmg")}: ({BDArmorySettings.BD_FIRE_DAMAGE}/s)", leftLabel); // "Fire Damage magnitude" + BDArmorySettings.BD_FIRE_DOT = GUI.Toggle(SLeftRect(++line, 1f), BDArmorySettings.BD_FIRE_DOT, StringUtils.Localize("#LOC_BDArmory_Settings_BD_DoT"));//"Fire Damage" + GUI.Label(SLeftSliderRect(++line, 1f), $"{StringUtils.Localize("#LOC_BDArmory_Settings_BD_Fire_Dmg")}: ({BDArmorySettings.BD_FIRE_DAMAGE}/s)", leftLabel); // "Fire Damage magnitude" BDArmorySettings.BD_FIRE_DAMAGE = Mathf.Round((GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.BD_FIRE_DAMAGE, 0f, 20))); - BDArmorySettings.BD_FIRE_FUELEX = GUI.Toggle(SLeftRect(++line, 1f), BDArmorySettings.BD_FIRE_FUELEX, Localizer.Format("#LOC_BDArmory_Settings_BD_FuelFireEX"));//"Fueltank Explosions - BDArmorySettings.BD_FIRE_HEATDMG = GUI.Toggle(SLeftRect(++line, 1f), BDArmorySettings.BD_FIRE_HEATDMG, Localizer.Format("#LOC_BDArmory_Settings_BD_FireHeat"));//"Fires add Heat + BDArmorySettings.BD_FIRE_FUELEX = GUI.Toggle(SLeftRect(++line, 1f), BDArmorySettings.BD_FIRE_FUELEX, StringUtils.Localize("#LOC_BDArmory_Settings_BD_FuelFireEX"));//"Fueltank Explosions + BDArmorySettings.BD_FIRE_HEATDMG = GUI.Toggle(SLeftRect(++line, 1f), BDArmorySettings.BD_FIRE_HEATDMG, StringUtils.Localize("#LOC_BDArmory_Settings_BD_FireHeat"));//"Fires add Heat } line += 0.5f; } } - if (GUI.Button(SLineRect(++line), (BDArmorySettings.RADAR_SETTINGS_TOGGLE ? "Hide " : "Show ") + Localizer.Format("#LOC_BDArmory_Settings_RadarSettingsToggle"))) // Show/hide Radar settings. + if (GUI.Button(SLineRect(++line), (BDArmorySettings.RADAR_SETTINGS_TOGGLE ? StringUtils.Localize("#LOC_BDArmory_Generic_Hide") : StringUtils.Localize("#LOC_BDArmory_Generic_Show")) + " " + StringUtils.Localize("#LOC_BDArmory_Settings_RadarSettingsToggle"))) // Show/hide Radar settings. { BDArmorySettings.RADAR_SETTINGS_TOGGLE = !BDArmorySettings.RADAR_SETTINGS_TOGGLE; } @@ -2987,7 +3538,7 @@ void WindowSettings(int windowID) { line += 0.2f; - GUI.Label(SLeftSliderRect(++line), Localizer.Format("#LOC_BDArmory_Settings_RWRWindowScale") + ": " + (BDArmorySettings.RWR_WINDOW_SCALE * 100).ToString("0") + "%", leftLabel); // RWR Window Scale + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_RWRWindowScale")}: {(BDArmorySettings.RWR_WINDOW_SCALE * 100):0} %", leftLabel); // RWR Window Scale float rwrScale = BDArmorySettings.RWR_WINDOW_SCALE; rwrScale = Mathf.Round(GUI.HorizontalSlider(SRightSliderRect(line), rwrScale, BDArmorySettings.RWR_WINDOW_SCALE_MIN, BDArmorySettings.RWR_WINDOW_SCALE_MAX) * 100.0f) * 0.01f; if (rwrScale.ToString(CultureInfo.InvariantCulture) != BDArmorySettings.RWR_WINDOW_SCALE.ToString(CultureInfo.InvariantCulture)) @@ -2995,7 +3546,7 @@ void WindowSettings(int windowID) ResizeRwrWindow(rwrScale); } - GUI.Label(SLeftSliderRect(++line), Localizer.Format("#LOC_BDArmory_Settings_RadarWindowScale") + ": " + (BDArmorySettings.RADAR_WINDOW_SCALE * 100).ToString("0") + "%", leftLabel); // Radar Window Scale + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_RadarWindowScale")}: {(BDArmorySettings.RADAR_WINDOW_SCALE * 100):0} %", leftLabel); // Radar Window Scale float radarScale = BDArmorySettings.RADAR_WINDOW_SCALE; radarScale = Mathf.Round(GUI.HorizontalSlider(SRightSliderRect(line), radarScale, BDArmorySettings.RADAR_WINDOW_SCALE_MIN, BDArmorySettings.RADAR_WINDOW_SCALE_MAX) * 100.0f) * 0.01f; if (radarScale.ToString(CultureInfo.InvariantCulture) != BDArmorySettings.RADAR_WINDOW_SCALE.ToString(CultureInfo.InvariantCulture)) @@ -3003,7 +3554,7 @@ void WindowSettings(int windowID) ResizeRadarWindow(radarScale); } - GUI.Label(SLeftSliderRect(++line), Localizer.Format("#LOC_BDArmory_Settings_TargetWindowScale") + ": " + (BDArmorySettings.TARGET_WINDOW_SCALE * 100).ToString("0") + "%", leftLabel); // Target Window Scale + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_TargetWindowScale")}: {(BDArmorySettings.TARGET_WINDOW_SCALE * 100):0} %", leftLabel); // Target Window Scale float targetScale = BDArmorySettings.TARGET_WINDOW_SCALE; targetScale = Mathf.Round(GUI.HorizontalSlider(SRightSliderRect(line), targetScale, BDArmorySettings.TARGET_WINDOW_SCALE_MIN, BDArmorySettings.TARGET_WINDOW_SCALE_MAX) * 100.0f) * 0.01f; if (targetScale.ToString(CultureInfo.InvariantCulture) != BDArmorySettings.TARGET_WINDOW_SCALE.ToString(CultureInfo.InvariantCulture)) @@ -3011,14 +3562,15 @@ void WindowSettings(int windowID) ResizeTargetWindow(targetScale); } - GUI.Label(SLeftRect(++line), Localizer.Format("#LOC_BDArmory_Settings_TargetWindowInvertMouse"), leftLabel); - BDArmorySettings.TARGET_WINDOW_INVERT_MOUSE_X = GUI.Toggle(SEighthRect(line,5), BDArmorySettings.TARGET_WINDOW_INVERT_MOUSE_X, "X"); - BDArmorySettings.TARGET_WINDOW_INVERT_MOUSE_Y = GUI.Toggle(SEighthRect(line,6), BDArmorySettings.TARGET_WINDOW_INVERT_MOUSE_Y, "Y"); + GUI.Label(SLeftRect(++line), StringUtils.Localize("#LOC_BDArmory_Settings_TargetWindowInvertMouse"), leftLabel); + BDArmorySettings.TARGET_WINDOW_INVERT_MOUSE_X = GUI.Toggle(SEighthRect(line, 5), BDArmorySettings.TARGET_WINDOW_INVERT_MOUSE_X, "X"); + BDArmorySettings.TARGET_WINDOW_INVERT_MOUSE_Y = GUI.Toggle(SEighthRect(line, 6), BDArmorySettings.TARGET_WINDOW_INVERT_MOUSE_Y, "Y"); + BDArmorySettings.LOGARITHMIC_RADAR_DISPLAY = GUI.Toggle(SLeftRect(++line), BDArmorySettings.LOGARITHMIC_RADAR_DISPLAY, StringUtils.Localize("#LOC_BDArmory_Settings_LogarithmicRWRDisplay")); //"Logarithmic RWR Display" line += 0.5f; } - if (GUI.Button(SLineRect(++line), (BDArmorySettings.OTHER_SETTINGS_TOGGLE ? "Hide " : "Show ") + Localizer.Format("#LOC_BDArmory_Settings_OtherSettingsToggle"))) // Show/hide Other settings. + if (GUI.Button(SLineRect(++line), (BDArmorySettings.OTHER_SETTINGS_TOGGLE ? StringUtils.Localize("#LOC_BDArmory_Generic_Hide") : StringUtils.Localize("#LOC_BDArmory_Generic_Show")) + " " + StringUtils.Localize("#LOC_BDArmory_Settings_OtherSettingsToggle"))) // Show/hide Other settings. { BDArmorySettings.OTHER_SETTINGS_TOGGLE = !BDArmorySettings.OTHER_SETTINGS_TOGGLE; } @@ -3026,10 +3578,10 @@ void WindowSettings(int windowID) { line += 0.2f; - GUI.Label(SLeftSliderRect(++line), Localizer.Format("#LOC_BDArmory_Settings_TriggerHold") + ": " + BDArmorySettings.TRIGGER_HOLD_TIME.ToString("0.00") + "s", leftLabel);//Trigger Hold + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_TriggerHold")}: {BDArmorySettings.TRIGGER_HOLD_TIME: 0} s", leftLabel);//Trigger Hold BDArmorySettings.TRIGGER_HOLD_TIME = GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.TRIGGER_HOLD_TIME, 0.02f, 1f); - GUI.Label(SLeftSliderRect(++line), Localizer.Format("#LOC_BDArmory_Settings_UIVolume") + ": " + (BDArmorySettings.BDARMORY_UI_VOLUME * 100).ToString("0"), leftLabel);//UI Volume + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_UIVolume")}: {(BDArmorySettings.BDARMORY_UI_VOLUME * 100):0}", leftLabel);//UI Volume float uiVol = BDArmorySettings.BDARMORY_UI_VOLUME; uiVol = GUI.HorizontalSlider(SRightSliderRect(line), uiVol, 0f, 1f); if (uiVol != BDArmorySettings.BDARMORY_UI_VOLUME && OnVolumeChange != null) @@ -3038,7 +3590,7 @@ void WindowSettings(int windowID) } BDArmorySettings.BDARMORY_UI_VOLUME = uiVol; - GUI.Label(SLeftSliderRect(++line), Localizer.Format("#LOC_BDArmory_Settings_WeaponVolume") + ": " + (BDArmorySettings.BDARMORY_WEAPONS_VOLUME * 100).ToString("0"), leftLabel);//Weapon Volume + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_WeaponVolume")}: {(BDArmorySettings.BDARMORY_WEAPONS_VOLUME * 100):0}", leftLabel);//Weapon Volume float weaponVol = BDArmorySettings.BDARMORY_WEAPONS_VOLUME; weaponVol = GUI.HorizontalSlider(SRightSliderRect(line), weaponVol, 0f, 1f); if (uiVol != BDArmorySettings.BDARMORY_WEAPONS_VOLUME && OnVolumeChange != null) @@ -3047,12 +3599,12 @@ void WindowSettings(int windowID) } BDArmorySettings.BDARMORY_WEAPONS_VOLUME = weaponVol; - if (BDArmorySettings.ADVANDED_USER_SETTINGS) + if (BDArmorySettings.ADVANCED_USER_SETTINGS) { - BDArmorySettings.TRACE_VESSELS_DURING_COMPETITIONS = GUI.Toggle(new Rect(settingsMargin, ++line * settingsLineHeight, 2f * (settingsWidth - 2f * settingsMargin) / 3f, settingsLineHeight), BDArmorySettings.TRACE_VESSELS_DURING_COMPETITIONS, Localizer.Format("#LOC_BDArmory_Settings_TraceVessels"));// Trace Vessels (custom 2/3 width) + BDArmorySettings.TRACE_VESSELS_DURING_COMPETITIONS = GUI.Toggle(new Rect(settingsMargin, ++line * settingsLineHeight, 2f * (settingsWidth - 2f * settingsMargin) / 3f, settingsLineHeight), BDArmorySettings.TRACE_VESSELS_DURING_COMPETITIONS, StringUtils.Localize("#LOC_BDArmory_Settings_TraceVessels"));// Trace Vessels (custom 2/3 width) if (LoadedVesselSwitcher.Instance != null) { - if (GUI.Button(SLineThirdRect(line, 2), LoadedVesselSwitcher.Instance.vesselTraceEnabled ? Localizer.Format("#LOC_BDArmory_Settings_TraceVesselsManualStop") : Localizer.Format("#LOC_BDArmory_Settings_TraceVesselsManualStart"))) + if (GUI.Button(SLineThirdRect(line, 2), LoadedVesselSwitcher.Instance.vesselTraceEnabled ? StringUtils.Localize("#LOC_BDArmory_Settings_TraceVesselsManualStop") : StringUtils.Localize("#LOC_BDArmory_Settings_TraceVesselsManualStart"))) { if (LoadedVesselSwitcher.Instance.vesselTraceEnabled) { LoadedVesselSwitcher.Instance.StopVesselTracing(); } @@ -3065,96 +3617,7 @@ void WindowSettings(int windowID) line += 0.5f; } -#if DEBUG // Only visible when compiled in Debug configuration. - if (GUI.Button(SLineRect(++line), (BDArmorySettings.DEBUG_SETTINGS_TOGGLE ? "Hide " : " Show ") + Localizer.Format("#LOC_BDArmory_Settings_DebugSettingsToggle"))) // Show/Hide Debug Settings - { - BDArmorySettings.DEBUG_SETTINGS_TOGGLE = !BDArmorySettings.DEBUG_SETTINGS_TOGGLE; - } - if (BDArmorySettings.DEBUG_SETTINGS_TOGGLE) - { - if (BDACompetitionMode.Instance != null) - { - if (GUI.Button(SLeftRect(++line), "Run DEBUG checks"))// Run DEBUG checks - { - switch (Event.current.button) - { - case 1: // right click - StartCoroutine(BDACompetitionMode.Instance.CheckGCPerformance()); - break; - default: - BDACompetitionMode.Instance.CleanUpKSPsDeadReferences(); - BDACompetitionMode.Instance.RunDebugChecks(); - break; - } - } - if (GUI.Button(SLeftRect(++line), "Test Vessel Module Registry")) - { - StartCoroutine(VesselModuleRegistry.Instance.PerformanceTest()); - } - } - // if (GUI.Button(SLineRect(++line), "timing test")) // Timing tests. - // { - // var test = FlightGlobals.ActiveVessel.transform.position; - // float FiringTolerance = 1f; - // float targetRadius = 20f; - // Vector3 finalAimTarget = new Vector3(10f, 20f, 30f); - // Vector3 pos = new Vector3(2f, 3f, 4f); - // float theta_const = Mathf.Deg2Rad * 1f; - // float test_out = 0f; - // int iters = 10000000; - // var now = Time.realtimeSinceStartup; - // for (int i = 0; i < iters; ++i) - // { - // test_out = i > iters ? 1f : 1f - 0.5f * FiringTolerance * FiringTolerance * targetRadius * targetRadius / (finalAimTarget - pos).sqrMagnitude; - // } - // Debug.Log("DEBUG sqrMagnitude " + (Time.realtimeSinceStartup - now) / iters + "s/iter, out: " + test_out); - // now = Time.realtimeSinceStartup; - // for (int i = 0; i < iters; ++i) - // { - // var theta = FiringTolerance * targetRadius / (finalAimTarget - pos).magnitude + theta_const; - // test_out = i > iters ? 1f : 1f - 0.5f * (theta * theta); - // } - // Debug.Log("DEBUG magnitude " + (Time.realtimeSinceStartup - now) / iters + "s/iter, out: " + test_out); - // } - if (GUI.Button(SLeftRect(++line), "Layer test")) - { - for (int i = 0; i < 32; ++i) - { - // Vector3 mouseAim = new Vector3(Input.mousePosition.x / Screen.width, Input.mousePosition.y / Screen.height, 0); - Ray ray = FlightCamera.fetch.mainCamera.ViewportPointToRay(new Vector3(0.5f, 0.5f, 0f)); - RaycastHit hit; - - if (Physics.Raycast(ray, out hit, 1000f, (1 << i))) - { - var hitPart = hit.collider.gameObject.GetComponentInParent(); - var hitEVA = hit.collider.gameObject.GetComponentUpwards(); - var hitBuilding = hit.collider.gameObject.GetComponentUpwards(); - if (hitEVA != null) hitPart = hitEVA.part; - if (hitPart != null) Debug.Log($"DEBUG Bitmask at {i} hit {hitPart.name}."); - else if (hitBuilding != null) Debug.Log($"DEBUG Bitmask at {i} hit {hitBuilding.name}"); - else Debug.Log($"DEBUG Bitmask at {i} hit {hit.collider.gameObject.name}"); - } - } - } - if (GUI.Button(SLeftRect(++line), "Test vessel position timing.")) - { StartCoroutine(TestVesselPositionTiming()); } - if (GUI.Button(SLeftRect(++line), "FS engine status")) - { - foreach (var vessel in FlightGlobals.VesselsLoaded) - FireSpitter.CheckStatus(vessel); - } - if (GUI.Button(SLeftRect(++line), "Spawn spawn probe here.")) - { - SpawnUtils.SpawnSpawnProbe(); - } - if (GUI.Button(SLeftRect(++line), "Quit KSP.")) - { - QuitKSP(); - } - } -#endif - - if (GUI.Button(SLineRect(++line), (BDArmorySettings.COMPETITION_SETTINGS_TOGGLE ? "Hide " : "Show ") + Localizer.Format("#LOC_BDArmory_Settings_CompSettingsToggle")))//Show/hide Competition settings. + if (GUI.Button(SLineRect(++line), $"{(BDArmorySettings.COMPETITION_SETTINGS_TOGGLE ? StringUtils.Localize("#LOC_BDArmory_Generic_Hide") : StringUtils.Localize("#LOC_BDArmory_Generic_Show"))} {StringUtils.Localize("#LOC_BDArmory_Settings_CompSettingsToggle")}"))//Show/hide Competition settings. { BDArmorySettings.COMPETITION_SETTINGS_TOGGLE = !BDArmorySettings.COMPETITION_SETTINGS_TOGGLE; } @@ -3162,24 +3625,26 @@ void WindowSettings(int windowID) { line += 0.2f; - BDArmorySettings.COMPETITION_CLOSE_SETTINGS_ON_COMPETITION_START = GUI.Toggle(SLineRect(++line), BDArmorySettings.COMPETITION_CLOSE_SETTINGS_ON_COMPETITION_START, Localizer.Format("#LOC_BDArmory_Settings_CompetitionCloseSettingsOnCompetitionStart")); + BDArmorySettings.COMPETITION_CLOSE_SETTINGS_ON_COMPETITION_START = GUI.Toggle(SLineRect(++line), BDArmorySettings.COMPETITION_CLOSE_SETTINGS_ON_COMPETITION_START, StringUtils.Localize("#LOC_BDArmory_Settings_CompetitionCloseSettingsOnCompetitionStart")); - if (BDArmorySettings.ADVANDED_USER_SETTINGS) + BDArmorySettings.COMPETITION_START_DESPITE_FAILURES = GUI.Toggle(SLineRect(++line), BDArmorySettings.COMPETITION_START_DESPITE_FAILURES, StringUtils.Localize("#LOC_BDArmory_Settings_CompetitionStartDespiteFailures")); + + if (BDArmorySettings.ADVANCED_USER_SETTINGS) { - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_DebrisCleanUpDelay")}: ({BDArmorySettings.DEBRIS_CLEANUP_DELAY}s)", leftLabel); // Debris Clean-up delay + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_DebrisCleanUpDelay")}: ({BDArmorySettings.DEBRIS_CLEANUP_DELAY}s)", leftLabel); // Debris Clean-up delay BDArmorySettings.DEBRIS_CLEANUP_DELAY = Mathf.Round(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.DEBRIS_CLEANUP_DELAY, 1f, 60f)); - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_CompetitionNonCompetitorRemovalDelay")}: ({(BDArmorySettings.COMPETITION_NONCOMPETITOR_REMOVAL_DELAY > 60 ? "Off" : BDArmorySettings.COMPETITION_NONCOMPETITOR_REMOVAL_DELAY + "s")})", leftLabel); // Non-competitor removal frequency + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_CompetitionNonCompetitorRemovalDelay")}: ({(BDArmorySettings.COMPETITION_NONCOMPETITOR_REMOVAL_DELAY > 60 ? "Off" : BDArmorySettings.COMPETITION_NONCOMPETITOR_REMOVAL_DELAY + "s")})", leftLabel); // Non-competitor removal frequency BDArmorySettings.COMPETITION_NONCOMPETITOR_REMOVAL_DELAY = Mathf.Round(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.COMPETITION_NONCOMPETITOR_REMOVAL_DELAY, 1f, 61f)); } - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_CompetitionDuration")}: ({(BDArmorySettings.COMPETITION_DURATION > 0 ? BDArmorySettings.COMPETITION_DURATION + (BDArmorySettings.COMPETITION_DURATION > 1 ? " mins" : " min") : "Unlimited")})", leftLabel); + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_CompetitionDuration")}: ({(BDArmorySettings.COMPETITION_DURATION > 0 ? BDArmorySettings.COMPETITION_DURATION + (BDArmorySettings.COMPETITION_DURATION > 1 ? " mins" : " min") : "Unlimited")})", leftLabel); BDArmorySettings.COMPETITION_DURATION = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.COMPETITION_DURATION, 0f, 15f)); - if (BDArmorySettings.ADVANDED_USER_SETTINGS) + if (BDArmorySettings.ADVANCED_USER_SETTINGS) { - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_CompetitionInitialGracePeriod")}: ({BDArmorySettings.COMPETITION_INITIAL_GRACE_PERIOD}s)", leftLabel); + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_CompetitionInitialGracePeriod")}: ({BDArmorySettings.COMPETITION_INITIAL_GRACE_PERIOD}s)", leftLabel); BDArmorySettings.COMPETITION_INITIAL_GRACE_PERIOD = Mathf.Round(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.COMPETITION_INITIAL_GRACE_PERIOD, 0f, 60f)); } - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_CompetitionFinalGracePeriod")}: ({(BDArmorySettings.COMPETITION_FINAL_GRACE_PERIOD > 60 ? "Inf" : BDArmorySettings.COMPETITION_FINAL_GRACE_PERIOD + "s")})", leftLabel); + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_CompetitionFinalGracePeriod")}: ({(BDArmorySettings.COMPETITION_FINAL_GRACE_PERIOD > 60 ? "Inf" : BDArmorySettings.COMPETITION_FINAL_GRACE_PERIOD + "s")})", leftLabel); BDArmorySettings.COMPETITION_FINAL_GRACE_PERIOD = Mathf.Round(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.COMPETITION_FINAL_GRACE_PERIOD, 0f, 61f)); { // Auto Start Competition NOW Delay @@ -3196,23 +3661,40 @@ void WindowSettings(int windowID) { startNowAfter = $"{BDArmorySettings.COMPETITION_START_NOW_AFTER * 10}s"; } - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_CompetitionStartNowAfter")}: ({startNowAfter})", leftLabel); + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_CompetitionStartNowAfter")}: ({startNowAfter})", leftLabel); BDArmorySettings.COMPETITION_START_NOW_AFTER = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.COMPETITION_START_NOW_AFTER, 0f, 11f)); } - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_CompetitionKillTimer")}: (" + (BDArmorySettings.COMPETITION_KILL_TIMER > 0 ? (BDArmorySettings.COMPETITION_KILL_TIMER + "s") : "Off") + ")", leftLabel); // FIXME the toggle and this slider could be merged + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_CompetitionKillTimer")}: (" + (BDArmorySettings.COMPETITION_KILL_TIMER > 0 ? (BDArmorySettings.COMPETITION_KILL_TIMER + "s") : "Off") + ")", leftLabel); // FIXME the toggle and this slider could be merged BDArmorySettings.COMPETITION_KILL_TIMER = Mathf.Round(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.COMPETITION_KILL_TIMER, 0, 60f)); - GUI.Label(SLeftRect(++line), Localizer.Format("#LOC_BDArmory_Settings_CompetitionDistance"));//"Competition Distance" - float cDist; - compDistGui = GUI.TextField(SRightRect(line, 1, true), compDistGui); - if (Single.TryParse(compDistGui, out cDist)) + GUI.Label(SLeftRect(++line), StringUtils.Localize("#LOC_BDArmory_Settings_CompetitionIntraTeamSeparation")); // Intra-team separation. + var intraTeamSepRect = SRightRect(line, 1, true); + compIntraTeamSeparationBase = GUI.TextField(new Rect(intraTeamSepRect.x, intraTeamSepRect.y, intraTeamSepRect.width / 4 + 2, intraTeamSepRect.height), compIntraTeamSeparationBase, 6, inputFieldStyle); + GUI.Label(new Rect(intraTeamSepRect.x + intraTeamSepRect.width / 4 + 2, intraTeamSepRect.y, 15, intraTeamSepRect.height), " + "); + compIntraTeamSeparationPerMember = GUI.TextField(new Rect(intraTeamSepRect.x + intraTeamSepRect.width / 4 + 17, intraTeamSepRect.y, intraTeamSepRect.width / 4 + 2, intraTeamSepRect.height), compIntraTeamSeparationPerMember, 6, inputFieldStyle); + GUI.Label(new Rect(intraTeamSepRect.x + intraTeamSepRect.width / 2 + 25, intraTeamSepRect.y, intraTeamSepRect.width / 2 - 25, intraTeamSepRect.height), StringUtils.Localize("#LOC_BDArmory_Settings_CompetitionIntraTeamSeparationPerMember")); + if (Single.TryParse(compIntraTeamSeparationBase, out float cIntraBase) && BDArmorySettings.COMPETITION_INTRA_TEAM_SEPARATION_BASE != cIntraBase) + { + cIntraBase = Mathf.Round(cIntraBase); BDArmorySettings.COMPETITION_INTRA_TEAM_SEPARATION_BASE = cIntraBase; + if (cIntraBase != 0) compIntraTeamSeparationBase = cIntraBase.ToString(); + } + if (Single.TryParse(compIntraTeamSeparationPerMember, out float cIntraPerMember) && BDArmorySettings.COMPETITION_INTRA_TEAM_SEPARATION_PER_MEMBER != cIntraPerMember) { - BDArmorySettings.COMPETITION_DISTANCE = (int)cDist; + cIntraPerMember = Mathf.Round(cIntraPerMember); BDArmorySettings.COMPETITION_INTRA_TEAM_SEPARATION_PER_MEMBER = cIntraPerMember; + if (cIntraPerMember != 0) compIntraTeamSeparationPerMember = cIntraPerMember.ToString(); + } + + GUI.Label(SLeftRect(++line), StringUtils.Localize("#LOC_BDArmory_Settings_CompetitionDistance"));//"Competition Distance" + compDistGui = GUI.TextField(SRightRect(line, 1, true), compDistGui, inputFieldStyle); + if (Single.TryParse(compDistGui, out float cDist) && BDArmorySettings.COMPETITION_DISTANCE != cDist) + { + cDist = Mathf.Round(cDist); BDArmorySettings.COMPETITION_DISTANCE = cDist; + if (cDist != 0) compDistGui = cDist.ToString(); } line += 0.2f; - if (GUI.Button(SLineRect(++line, 1, true), (BDArmorySettings.GM_SETTINGS_TOGGLE ? "Hide " : "Show ") + Localizer.Format("#LOC_BDArmory_Settings_GMSettingsToggle")))//Show/hide slider settings. + if (GUI.Button(SLineRect(++line, 1, true), (BDArmorySettings.GM_SETTINGS_TOGGLE ? StringUtils.Localize("#LOC_BDArmory_Generic_Hide") : StringUtils.Localize("#LOC_BDArmory_Generic_Show")) + " " + StringUtils.Localize("#LOC_BDArmory_Settings_GMSettingsToggle")))//Show/hide slider settings. { BDArmorySettings.GM_SETTINGS_TOGGLE = !BDArmorySettings.GM_SETTINGS_TOGGLE; } @@ -3226,7 +3708,7 @@ void WindowSettings(int windowID) else if (BDArmorySettings.COMPETITION_ALTITUDE_LIMIT_HIGH < 20f) killerGMMaxAltitudeText = Mathf.RoundToInt(BDArmorySettings.COMPETITION_ALTITUDE_LIMIT_HIGH * 100f) + "m"; else if (BDArmorySettings.COMPETITION_ALTITUDE_LIMIT_HIGH < 39f) killerGMMaxAltitudeText = Mathf.RoundToInt(BDArmorySettings.COMPETITION_ALTITUDE_LIMIT_HIGH - 18f) + "km"; else killerGMMaxAltitudeText = Mathf.RoundToInt((BDArmorySettings.COMPETITION_ALTITUDE_LIMIT_HIGH - 38f) * 5f + 20f) + "km"; - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_CompetitionAltitudeLimitHigh")}: ({killerGMMaxAltitudeText})", leftLabel); + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_CompetitionAltitudeLimitHigh")}: ({killerGMMaxAltitudeText})", leftLabel); BDArmorySettings.COMPETITION_ALTITUDE_LIMIT_HIGH = Mathf.Round(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.COMPETITION_ALTITUDE_LIMIT_HIGH, 1f, 55f)); } { // Killer GM Min Altitude @@ -3238,32 +3720,35 @@ void WindowSettings(int windowID) else if (BDArmorySettings.COMPETITION_ALTITUDE_LIMIT_LOW < 20f) killerGMMinAltitudeText = Mathf.RoundToInt(BDArmorySettings.COMPETITION_ALTITUDE_LIMIT_LOW * 100f) + "m"; // 0m — 1900m @ 100m else if (BDArmorySettings.COMPETITION_ALTITUDE_LIMIT_LOW < 39f) killerGMMinAltitudeText = Mathf.RoundToInt(BDArmorySettings.COMPETITION_ALTITUDE_LIMIT_LOW - 18f) + "km"; // 2km — 20km @ 1km else killerGMMinAltitudeText = Mathf.RoundToInt((BDArmorySettings.COMPETITION_ALTITUDE_LIMIT_LOW - 38f) * 5f + 20f) + "km"; // 25km — 50km @ 5km - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_CompetitionAltitudeLimitLow")}: ({killerGMMinAltitudeText})", leftLabel); + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_CompetitionAltitudeLimitLow")}: ({killerGMMinAltitudeText})", leftLabel); BDArmorySettings.COMPETITION_ALTITUDE_LIMIT_LOW = Mathf.Round(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.COMPETITION_ALTITUDE_LIMIT_LOW, -39f, 44f)); } + BDArmorySettings.COMPETITION_ALTITUDE__LIMIT_ASL = GUI.Toggle(SLineRect(++line), BDArmorySettings.COMPETITION_ALTITUDE__LIMIT_ASL, "Use Absolute Altitude?"); // StringUtils.Localize("#LLOC_BDArmory_Settings_CompetitionAltitudeLimitASL")); if (BDArmorySettings.RUNWAY_PROJECT) { - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_CompetitionKillerGMGracePeriod")}: ({BDArmorySettings.COMPETITION_KILLER_GM_GRACE_PERIOD}s)", leftLabel); + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_CompetitionKillerGMGracePeriod")}: ({BDArmorySettings.COMPETITION_KILLER_GM_GRACE_PERIOD}s)", leftLabel); BDArmorySettings.COMPETITION_KILLER_GM_GRACE_PERIOD = Mathf.Round(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.COMPETITION_KILLER_GM_GRACE_PERIOD / 10f, 0f, 18f)) * 10f; - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_CompetitionKillerGMFrequency")}: ({(BDArmorySettings.COMPETITION_KILLER_GM_FREQUENCY > 60 ? "Off" : BDArmorySettings.COMPETITION_KILLER_GM_FREQUENCY + "s")}, {(BDACompetitionMode.Instance != null && BDACompetitionMode.Instance.killerGMenabled ? "on" : "off")})", leftLabel); + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_CompetitionKillerGMFrequency")}: ({(BDArmorySettings.COMPETITION_KILLER_GM_FREQUENCY > 60 ? "Off" : BDArmorySettings.COMPETITION_KILLER_GM_FREQUENCY + "s")}, {(BDACompetitionMode.Instance != null && BDACompetitionMode.Instance.killerGMenabled ? "on" : "off")})", leftLabel); BDArmorySettings.COMPETITION_KILLER_GM_FREQUENCY = Mathf.Round(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.COMPETITION_KILLER_GM_FREQUENCY / 10f, 1, 6)) * 10f; // For now, don't control the killerGMEnabled flag (it's controlled by right clicking M). } + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_CompetitionWaypointTimeThreshold")}: ({(BDArmorySettings.COMPETITION_WAYPOINTS_GM_KILL_PERIOD > 0 ? $"{BDArmorySettings.COMPETITION_WAYPOINTS_GM_KILL_PERIOD:0}s" : "Off")})", leftLabel); // Waypoint threshold + BDArmorySettings.COMPETITION_WAYPOINTS_GM_KILL_PERIOD = BDAMath.RoundToUnit(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.COMPETITION_WAYPOINTS_GM_KILL_PERIOD, 0, 120), 5); line += 0.2f; } if (BDArmorySettings.REMOTE_LOGGING_VISIBLE) { - if (GUI.Button(SLineRect(++line, 1, true), Localizer.Format(BDArmorySettings.REMOTE_LOGGING_ENABLED ? "#LOC_BDArmory_Disable" : "#LOC_BDArmory_Enable") + " " + Localizer.Format("#LOC_BDArmory_Settings_RemoteLogging"))) + if (GUI.Button(SLineRect(++line, 1, true), StringUtils.Localize(BDArmorySettings.REMOTE_LOGGING_ENABLED ? "#LOC_BDArmory_Disable" : "#LOC_BDArmory_Enable") + " " + StringUtils.Localize("#LOC_BDArmory_Settings_RemoteLogging"))) { BDArmorySettings.REMOTE_LOGGING_ENABLED = !BDArmorySettings.REMOTE_LOGGING_ENABLED; } if (BDArmorySettings.REMOTE_LOGGING_ENABLED) { - GUI.Label(SLeftRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_CompetitionID")}: ", leftLabel); // Competition hash. - BDArmorySettings.COMPETITION_HASH = GUI.TextField(SRightRect(line, 1, true), BDArmorySettings.COMPETITION_HASH); - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_RemoteInterheatDelay")}: ({BDArmorySettings.REMOTE_INTERHEAT_DELAY}s)", leftLabel); // Inter-heat delay + GUI.Label(SLeftRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_CompetitionID")}: ", leftLabel); // Competition hash. + BDArmorySettings.COMPETITION_HASH = GUI.TextField(SRightRect(line, 1, true), BDArmorySettings.COMPETITION_HASH, inputFieldStyle); + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_RemoteInterheatDelay")}: ({BDArmorySettings.REMOTE_INTERHEAT_DELAY}s)", leftLabel); // Inter-heat delay BDArmorySettings.REMOTE_INTERHEAT_DELAY = Mathf.Round(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.REMOTE_INTERHEAT_DELAY, 1f, 30f)); } } @@ -3279,22 +3764,22 @@ void WindowSettings(int windowID) { line += 0.5f; - GUI.Label(SLineRect(++line), "=== " + Localizer.Format("#LOC_BDArmory_Settings_DogfightCompetition") + " ===", centerLabel);//Dogfight Competition + GUI.Label(SLineRect(++line), $"=== {StringUtils.Localize("#LOC_BDArmory_Settings_DogfightCompetition")} ===", centerLabel);//Dogfight Competition if (BDACompetitionMode.Instance.competitionIsActive) { - if (GUI.Button(SLineRect(++line), Localizer.Format("#LOC_BDArmory_Settings_StopCompetition"))) // Stop competition. + if (GUI.Button(SLineRect(++line), StringUtils.Localize("#LOC_BDArmory_Settings_StopCompetition"))) // Stop competition. { BDACompetitionMode.Instance.StopCompetition(); } } else if (BDACompetitionMode.Instance.competitionStarting) { - GUI.Label(SLineRect(++line), Localizer.Format("#LOC_BDArmory_Settings_CompetitionStarting") + " (" + compDistGui + ")");//Starting Competition... - if (GUI.Button(SLeftButtonRect(++line), Localizer.Format("#LOC_BDArmory_Generic_Cancel")))//"Cancel" + GUI.Label(SLineRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_CompetitionStarting")} ({compDistGui})");//Starting Competition... + if (GUI.Button(SLeftButtonRect(++line), StringUtils.Localize("#LOC_BDArmory_Generic_Cancel")))//"Cancel" { BDACompetitionMode.Instance.StopCompetition(); } - if (GUI.Button(SRightButtonRect(line), Localizer.Format("#LOC_BDArmory_Settings_StartCompetitionNow"))) // Start competition NOW button. + if (GUI.Button(SRightButtonRect(line), StringUtils.Localize("#LOC_BDArmory_Settings_StartCompetitionNow"))) // Start competition NOW button. { BDACompetitionMode.Instance.StartCompetitionNow(); if (BDArmorySettings.COMPETITION_CLOSE_SETTINGS_ON_COMPETITION_START) CloseSettingsWindow(); @@ -3304,7 +3789,7 @@ void WindowSettings(int windowID) { if (BDArmorySettings.REMOTE_LOGGING_ENABLED) { - if (GUI.Button(SLineRect(++line), Localizer.Format("#LOC_BDArmory_Settings_RemoteSync"))) // Run Via Remote Orchestration + if (GUI.Button(SLineRect(++line), StringUtils.Localize("#LOC_BDArmory_Settings_RemoteSync"))) // Run Via Remote Orchestration { string vesselPath = Path.Combine(KSPUtil.ApplicationRootPath, "AutoSpawn"); if (!System.IO.Directory.Exists(vesselPath)) @@ -3317,19 +3802,19 @@ void WindowSettings(int windowID) } else { - string startCompetitionText = Localizer.Format("#LOC_BDArmory_Settings_StartCompetition"); + string startCompetitionText = StringUtils.Localize("#LOC_BDArmory_Settings_StartCompetition"); if (BDArmorySettings.RUNWAY_PROJECT) { switch (BDArmorySettings.RUNWAY_PROJECT_ROUND) { case 33: - startCompetitionText = Localizer.Format("#LOC_BDArmory_Settings_StartRapidDeployment"); + startCompetitionText = StringUtils.Localize("#LOC_BDArmory_Settings_StartRapidDeployment"); break; case 44: - startCompetitionText = Localizer.Format("#LOC_BDArmory_Settings_LowGravDeployment"); + startCompetitionText = StringUtils.Localize("#LOC_BDArmory_Settings_LowGravDeployment"); break; - case 60: // FIXME temporary index, to be assigned later - startCompetitionText = Localizer.Format("#LOC_BDArmory_Settings_StartOrbitalDeployment"); + case 53: // FIXME temporary index, to be assigned later + startCompetitionText = StringUtils.Localize("#LOC_BDArmory_Settings_StartOrbitalDeployment"); break; } } @@ -3348,16 +3833,16 @@ void WindowSettings(int windowID) case 44: BDACompetitionMode.Instance.StartRapidDeployment(0); break; - case 60: // FIXME temporary index, to be assigned later + case 53: BDACompetitionMode.Instance.StartRapidDeployment(0); break; default: - BDACompetitionMode.Instance.StartCompetitionMode(BDArmorySettings.COMPETITION_DISTANCE); + BDACompetitionMode.Instance.StartCompetitionMode(BDArmorySettings.COMPETITION_DISTANCE, BDArmorySettings.COMPETITION_START_DESPITE_FAILURES); break; } } else - BDACompetitionMode.Instance.StartCompetitionMode(BDArmorySettings.COMPETITION_DISTANCE); + BDACompetitionMode.Instance.StartCompetitionMode(BDArmorySettings.COMPETITION_DISTANCE, BDArmorySettings.COMPETITION_START_DESPITE_FAILURES); if (BDArmorySettings.COMPETITION_CLOSE_SETTINGS_ON_COMPETITION_START) CloseSettingsWindow(); } } @@ -3365,12 +3850,12 @@ void WindowSettings(int windowID) } ++line; - if (GUI.Button(SLineRect(++line), Localizer.Format("#LOC_BDArmory_Settings_EditInputs")))//"Edit Inputs" + if (GUI.Button(SLineRect(++line), StringUtils.Localize("#LOC_BDArmory_Settings_EditInputs")))//"Edit Inputs" { editKeys = true; } line += 0.5f; - if (!BDKeyBinder.current && GUI.Button(SLineRect(++line), Localizer.Format("#LOC_BDArmory_Generic_SaveandClose")))//"Save and Close" + if (!BDKeyBinder.current && GUI.Button(SLineRect(++line), StringUtils.Localize("#LOC_BDArmory_Generic_SaveandClose")))//"Save and Close" { SaveConfig(); windowSettingsEnabled = false; @@ -3379,8 +3864,8 @@ void WindowSettings(int windowID) line += 1.5f; // Bottom internal margin settingsHeight = (line * settingsLineHeight); WindowRectSettings.height = settingsHeight; - BDGUIUtils.RepositionWindow(ref WindowRectSettings); - BDGUIUtils.UseMouseEventInRect(WindowRectSettings); + GUIUtils.RepositionWindow(ref WindowRectSettings); + GUIUtils.UseMouseEventInRect(WindowRectSettings); } void CloseSettingsWindow() @@ -3433,35 +3918,35 @@ void InputSettings() settingsWidth = origSettingsWidth - 2 * settingsMargin; settingsHeight = origSettingsHeight - 100; Rect viewRect = new Rect(2, 20, settingsWidth + GUI.skin.verticalScrollbar.fixedWidth, settingsHeight); - Rect scrollerRect = new Rect(0, 0, settingsWidth - GUI.skin.verticalScrollbar.fixedWidth - 1, inputFields != null ? (inputFields.Length + 11) * settingsLineHeight : settingsHeight); + Rect scrollerRect = new Rect(0, 0, settingsWidth - GUI.skin.verticalScrollbar.fixedWidth - 1, inputFields != null ? (inputFields.Length + 13) * settingsLineHeight : settingsHeight); _displayViewerPosition = GUI.BeginScrollView(viewRect, _displayViewerPosition, scrollerRect, false, true); - GUI.Label(SLineRect(line++), "- " + Localizer.Format("#LOC_BDArmory_InputSettings_GUI") + " -", centerLabel); //GUI + GUI.Label(SLineRect(line++), $"- {StringUtils.Localize("#LOC_BDArmory_InputSettings_GUI")} -", centerLabel); //GUI InputSettingsList("GUI_", ref inputID, ref line); ++line; - GUI.Label(SLineRect(line++), "- " + Localizer.Format("#LOC_BDArmory_InputSettings_Weapons") + " -", centerLabel);//Weapons + GUI.Label(SLineRect(line++), $"- {StringUtils.Localize("#LOC_BDArmory_InputSettings_Weapons")} -", centerLabel);//Weapons InputSettingsList("WEAP_", ref inputID, ref line); ++line; - GUI.Label(SLineRect(line++), "- " + Localizer.Format("#LOC_BDArmory_InputSettings_TargetingPod") + " -", centerLabel);//Targeting Pod + GUI.Label(SLineRect(line++), $"- {StringUtils.Localize("#LOC_BDArmory_InputSettings_TargetingPod")} -", centerLabel);//Targeting Pod InputSettingsList("TGP_", ref inputID, ref line); ++line; - GUI.Label(SLineRect(line++), "- " + Localizer.Format("#LOC_BDArmory_InputSettings_Radar") + " -", centerLabel);//Radar + GUI.Label(SLineRect(line++), $"- {StringUtils.Localize("#LOC_BDArmory_InputSettings_Radar")} -", centerLabel);//Radar InputSettingsList("RADAR_", ref inputID, ref line); ++line; - GUI.Label(SLineRect(line++), "- " + Localizer.Format("#LOC_BDArmory_InputSettings_VesselSwitcher") + " -", centerLabel);//Vessel Switcher + GUI.Label(SLineRect(line++), $"- {StringUtils.Localize("#LOC_BDArmory_InputSettings_VesselSwitcher")} -", centerLabel);//Vessel Switcher InputSettingsList("VS_", ref inputID, ref line); ++line; - GUI.Label(SLineRect(line++), "- " + Localizer.Format("#LOC_BDArmory_InputSettings_Tournament") + " -", centerLabel);//Tournament + GUI.Label(SLineRect(line++), $"- {StringUtils.Localize("#LOC_BDArmory_InputSettings_Tournament")} -", centerLabel);//Tournament InputSettingsList("TOURNAMENT_", ref inputID, ref line); ++line; - GUI.Label(SLineRect(line++), "- " + Localizer.Format("#LOC_BDArmory_InputSettings_TimeScaling") + " -", centerLabel);//Time Scaling + GUI.Label(SLineRect(line++), $"- {StringUtils.Localize("#LOC_BDArmory_InputSettings_TimeScaling")} -", centerLabel);//Time Scaling InputSettingsList("TIME_", ref inputID, ref line); GUI.EndScrollView(); @@ -3469,14 +3954,14 @@ void InputSettings() line += 2; settingsWidth = origSettingsWidth; settingsMargin = origSettingsMargin; - if (!BDKeyBinder.current && GUI.Button(SLineRect(line), Localizer.Format("#LOC_BDArmory_InputSettings_BackBtn")))//"Back" + if (!BDKeyBinder.current && GUI.Button(SLineRect(line), StringUtils.Localize("#LOC_BDArmory_InputSettings_BackBtn")))//"Back" { editKeys = false; } settingsHeight = origSettingsHeight; WindowRectSettings.height = origSettingsHeight; - BDGUIUtils.UseMouseEventInRect(WindowRectSettings); + GUIUtils.UseMouseEventInRect(WindowRectSettings); } void InputSettingsList(string prefix, ref int id, ref float line) @@ -3508,7 +3993,7 @@ void InputSettingsLine(string fieldName, int id, ref float line) typeof(BDInputSettingsFields).GetField(fieldName).SetValue(null, recorded); } - label = " " + Localizer.Format("#LOC_BDArmory_InputSettings_recordedInput");//Press a key or button. + label = $" {StringUtils.Localize("#LOC_BDArmory_InputSettings_recordedInput")}";//Press a key or button. } else { @@ -3525,24 +4010,23 @@ void InputSettingsLine(string fieldName, int id, ref float line) } label = " " + inputInfo.description + " : " + inputInfo.inputString; - if (GUI.Button(SSetKeyRect(line), Localizer.Format("#LOC_BDArmory_InputSettings_SetKey")))//"Set Key" + if (GUI.Button(SSetKeyRect(line), StringUtils.Localize("#LOC_BDArmory_InputSettings_SetKey")))//"Set Key" { BDKeyBinder.BindKey(id); } - if (GUI.Button(SClearKeyRect(line), Localizer.Format("#LOC_BDArmory_InputSettings_Clear")))//"Clear" + if (GUI.Button(SClearKeyRect(line), StringUtils.Localize("#LOC_BDArmory_InputSettings_Clear")))//"Clear" { typeof(BDInputSettingsFields).GetField(fieldName) .SetValue(null, new BDInputInfo(inputInfo.description)); } } - GUI.Label(SLeftRect(line), label); + GUI.Label(SLineThirdRect(line, 0, 2), label); line++; } Rect SSetKeyRect(float line) { - return new Rect(settingsMargin + (2 * (settingsWidth - 2 * settingsMargin) / 3), line * settingsLineHeight, - (settingsWidth - (2 * settingsMargin)) / 6, settingsLineHeight); + return new Rect(settingsMargin + (2 * (settingsWidth - 2 * settingsMargin) / 3), line * settingsLineHeight, (settingsWidth - (2 * settingsMargin)) / 6, settingsLineHeight); } Rect SClearKeyRect(float line) @@ -3559,12 +4043,14 @@ void HideGameUI() { GAME_UI_ENABLED = false; BDACompetitionMode.Instance.UpdateGUIElements(); + UpdateCursorState(); } void ShowGameUI() { GAME_UI_ENABLED = true; BDACompetitionMode.Instance.UpdateGUIElements(); + UpdateCursorState(); } internal void OnDestroy() @@ -3585,7 +4071,7 @@ internal void OnDestroy() void OnVesselGoOffRails(Vessel v) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_OTHER) { Debug.Log("[BDArmory.BDArmorySetup]: Loaded vessel: " + v.vesselName + ", Velocity: " + v.Velocity() + ", packed: " + v.packed); //v.SetWorldVelocity(Vector3d.zero); @@ -3600,12 +4086,8 @@ public void SaveVolumeSettings() } #if DEBUG - public static void QuitKSP() - { - HighLogic.LoadScene(GameScenes.MAINMENU); - Application.Quit(); - } - + // static int PROF_N_pow = 5, PROF_n_pow = 2; + static int PROF_N = 100000, PROF_n = 100; IEnumerator TestVesselPositionTiming() { var wait = new WaitForFixedUpdate(); @@ -3631,15 +4113,282 @@ IEnumerator TestVesselPositionTiming() TimingManager.FixedUpdateRemove(TimingManager.TimingStage.Late, Late); TimingManager.FixedUpdateRemove(TimingManager.TimingStage.BetterLateThanNever, BetterLateThanNever); } - void ObscenelyEarly() { Debug.Log($"DEBUG {Time.time} ObscenelyEarly, active vessel position: {FlightGlobals.ActiveVessel.transform.position.ToString("G6")}"); } - void Early() { Debug.Log($"DEBUG {Time.time} Early, active vessel position: {FlightGlobals.ActiveVessel.transform.position.ToString("G6")}"); } - void Precalc() { Debug.Log($"DEBUG {Time.time} Precalc, active vessel position: {FlightGlobals.ActiveVessel.transform.position.ToString("G6")}"); } - void Earlyish() { Debug.Log($"DEBUG {Time.time} Earlyish, active vessel position: {FlightGlobals.ActiveVessel.transform.position.ToString("G6")}"); } - void Normal() { Debug.Log($"DEBUG {Time.time} Normal, active vessel position: {FlightGlobals.ActiveVessel.transform.position.ToString("G6")}"); } - void FashionablyLate() { Debug.Log($"DEBUG {Time.time} FashionablyLate, active vessel position: {FlightGlobals.ActiveVessel.transform.position.ToString("G6")}"); } - void FlightIntegrator() { Debug.Log($"DEBUG {Time.time} FlightIntegrator, active vessel position: {FlightGlobals.ActiveVessel.transform.position.ToString("G6")}"); } - void Late() { Debug.Log($"DEBUG {Time.time} Late, active vessel position: {FlightGlobals.ActiveVessel.transform.position.ToString("G6")}"); } - void BetterLateThanNever() { Debug.Log($"DEBUG {Time.time} BetterLateThanNever, active vessel position: {FlightGlobals.ActiveVessel.transform.position.ToString("G6")}"); } + void ObscenelyEarly() { Debug.Log($"DEBUG {Time.time} ObscenelyEarly, active vessel position: {FlightGlobals.ActiveVessel.transform.position.ToString("G6")}, KbFV: {Krakensbane.GetFrameVelocityV3f()}"); } + void Early() { Debug.Log($"DEBUG {Time.time} Early, active vessel position: {FlightGlobals.ActiveVessel.transform.position.ToString("G6")}, KbFV: {Krakensbane.GetFrameVelocityV3f()}"); } + void Precalc() { Debug.Log($"DEBUG {Time.time} Precalc, active vessel position: {FlightGlobals.ActiveVessel.transform.position.ToString("G6")}, KbFV: {Krakensbane.GetFrameVelocityV3f()}"); } + void Earlyish() { Debug.Log($"DEBUG {Time.time} Earlyish, active vessel position: {FlightGlobals.ActiveVessel.transform.position.ToString("G6")}, KbFV: {Krakensbane.GetFrameVelocityV3f()}"); } + void Normal() { Debug.Log($"DEBUG {Time.time} Normal, active vessel position: {FlightGlobals.ActiveVessel.transform.position.ToString("G6")}, KbFV: {Krakensbane.GetFrameVelocityV3f()}"); } + void FashionablyLate() { Debug.Log($"DEBUG {Time.time} FashionablyLate, active vessel position: {FlightGlobals.ActiveVessel.transform.position.ToString("G6")}, KbFV: {Krakensbane.GetFrameVelocityV3f()}"); } + void FlightIntegrator() { Debug.Log($"DEBUG {Time.time} FlightIntegrator, active vessel position: {FlightGlobals.ActiveVessel.transform.position.ToString("G6")}, KbFV: {Krakensbane.GetFrameVelocityV3f()}"); } + void Late() { Debug.Log($"DEBUG {Time.time} Late, active vessel position: {FlightGlobals.ActiveVessel.transform.position.ToString("G6")}, KbFV: {Krakensbane.GetFrameVelocityV3f()}"); } + void BetterLateThanNever() { Debug.Log($"DEBUG {Time.time} BetterLateThanNever, active vessel position: {FlightGlobals.ActiveVessel.transform.position.ToString("G6")}, KbFV: {Krakensbane.GetFrameVelocityV3f()}"); } + + IEnumerator TestYieldWaitLengths() + { + Debug.Log($"DEBUG Starting yield wait tests at {Time.time} with timeScale {Time.timeScale}"); + var tic = Time.time; + for (int i = 0; i < 3; ++i) + { + yield return new WaitForFixedUpdate(); + Debug.Log($"DEBUG WaitForFixedUpdate took {Time.time - tic}s at {Time.time}"); + tic = Time.time; + } + for (int i = 0; i < 3; ++i) + { + yield return null; + Debug.Log($"DEBUG yield null took {Time.time - tic}s at {Time.time}"); + tic = Time.time; + } + yield return new WaitForSeconds(1); + Debug.Log($"DEBUG WaitForSeconds(1) took {Time.time - tic}s at {Time.time}"); + tic = Time.time; + yield return new WaitForSecondsFixed(1); + Debug.Log($"DEBUG WaitForSecondsFixed(1) took {Time.time - tic}s at {Time.time}"); + tic = Time.time; + yield return new WaitUntil(() => Time.time - tic > 1); + Debug.Log($"DEBUG WaitUntil took {Time.time - tic}s at {Time.time}"); + tic = Time.time; + yield return new WaitUntilFixed(() => Time.time - tic > 1); + Debug.Log($"DEBUG WaitUntilFixed took {Time.time - tic}s at {Time.time}"); + } + + IEnumerator TestLocalization() + { + int N = 1 << 18; // With stack traces enabled, this takes around 30s and gives ~8MB GC alloc for the Localizer.Format and 0 for StringUtils.Localize. + var tic = Time.realtimeSinceStartup; + string result = ""; + for (int i = 0; i < N; ++i) + result = Localizer.Format("#LOC_BDArmory_Settings_GUIBackgroundOpacity"); + var dt = Time.realtimeSinceStartup - tic; + Debug.Log($"DEBUG Result {result} with Localizer.Format took {dt / N:G3}s"); + yield return null; + yield return null; + tic = Time.realtimeSinceStartup; + result = ""; + for (int i = 0; i < N; ++i) + result = StringUtils.Localize("#LOC_BDArmory_Settings_GUIBackgroundOpacity"); + dt = Time.realtimeSinceStartup - tic; + Debug.Log($"DEBUG Result {result} with StringUtils.Localize took {dt / N:G3}s"); + } + + IEnumerator TestRaycastHitMergeAndSort() + { + RaycastHit[] forwardHits = new RaycastHit[100]; + RaycastHit[] reverseHits = new RaycastHit[100]; + RaycastHit[] sortedHits2 = new RaycastHit[200]; + int forwardHitCount = 97, reverseHitCount = 13; + for (int i = 0; i < forwardHitCount; ++i) forwardHits[i].distance = UnityEngine.Random.Range(0f, 1f); + for (int i = 0; i < reverseHitCount; ++i) reverseHits[i].distance = UnityEngine.Random.Range(0f, 1f); + List sortedHits = forwardHits.Take(forwardHitCount).Concat(reverseHits.Take(reverseHitCount)).ToList(); + yield return null; + yield return null; + int N = 10000; + var tic = Time.realtimeSinceStartup; + for (int i = 0; i < N; ++i) + { + sortedHits.Clear(); + sortedHits.AddRange(forwardHits.Take(forwardHitCount).Concat(reverseHits.Take(reverseHitCount))); + sortedHits.Sort((x1, x2) => x1.distance.CompareTo(x2.distance)); + } + var dt = Time.realtimeSinceStartup - tic; + Debug.Log($"DEBUG Clear->AddRange->Sort took {dt / N:G3}s"); + yield return null; + yield return null; + tic = Time.realtimeSinceStartup; + for (int i = 0; i < N; ++i) + { + Array.Copy(forwardHits, sortedHits2, forwardHitCount); + Array.Copy(reverseHits, 0, sortedHits2, forwardHitCount, reverseHitCount); + Array.Sort(sortedHits2, 0, forwardHitCount + reverseHitCount, RaycastHitComparer.raycastHitComparer); + } + dt = Time.realtimeSinceStartup - tic; + Debug.Log($"DEBUG Array.Copy -> Sort took {dt / N:G3}s"); // This seems to be the fastest and causes the least amount of GC. + yield return null; + yield return null; + tic = Time.realtimeSinceStartup; + for (int i = 0; i < N; ++i) + { + sortedHits = forwardHits.Take(forwardHitCount).Concat(reverseHits.Take(reverseHitCount)).ToList(); + sortedHits.Sort((x1, x2) => x1.distance.CompareTo(x2.distance)); + } + dt = Time.realtimeSinceStartup - tic; + Debug.Log($"DEBUG ToList->Sort took {dt / N:G3}s"); + yield return null; + yield return null; + tic = Time.realtimeSinceStartup; + for (int i = 0; i < N; ++i) + { + sortedHits = forwardHits.Take(forwardHitCount).Concat(reverseHits.Take(reverseHitCount)).OrderBy(x => x.distance).ToList(); + } + dt = Time.realtimeSinceStartup - tic; + Debug.Log($"DEBUG OrderBy->ToList took {dt / N:G3}s"); + yield return null; + yield return null; + tic = Time.realtimeSinceStartup; + for (int i = 0; i < N; ++i) + { + sortedHits.Clear(); + sortedHits.AddRange(forwardHits.Take(forwardHitCount).Concat(reverseHits.Take(reverseHitCount)).OrderBy(x => x.distance)); + } + dt = Time.realtimeSinceStartup - tic; + Debug.Log($"DEBUG OrderBy->AddRange took {dt / N:G3}s"); + } + + IEnumerator TestVesselName() + { + int N = 1 << 24; + var tic = Time.realtimeSinceStartup; + string result = ""; + var vessel = FlightGlobals.ActiveVessel; + if (vessel is null) yield break; + yield return null; + yield return null; + for (int i = 0; i < N; ++i) + result = vessel.vesselName; + var dt = Time.realtimeSinceStartup - tic; + Debug.Log($"DEBUG Name: {result} with vessel.vesselName took {dt / N:G3}s"); + yield return null; + yield return null; + tic = Time.realtimeSinceStartup; + result = ""; + for (int i = 0; i < N; ++i) + result = vessel.GetName(); + dt = Time.realtimeSinceStartup - tic; + Debug.Log($"DEBUG Name {result} with vessel.GetName() took {dt / N:G3}s"); + yield return null; + yield return null; + tic = Time.realtimeSinceStartup; + result = ""; + for (int i = 0; i < N; ++i) + result = vessel.GetDisplayName(); + dt = Time.realtimeSinceStartup - tic; + Debug.Log($"DEBUG Name {result} with vessel.GetDisplayName() took {dt / N:G3}s"); + } + + IEnumerator TestGetAudioClip() + { + int N = 1 << 16; + var tic = Time.realtimeSinceStartup; + AudioClip clip; + var vessel = FlightGlobals.ActiveVessel; + if (vessel is null) yield break; + yield return null; + yield return null; + for (int i = 0; i < N; ++i) + clip = GameDatabase.Instance.GetAudioClip("BDArmory/Sounds/deployClick"); + var dt = Time.realtimeSinceStartup - tic; + Debug.Log($"DEBUG GetAudioClip took {dt / N:G3}s"); + yield return null; + yield return null; + tic = Time.realtimeSinceStartup; + clip = null; + for (int i = 0; i < N; ++i) + clip = SoundUtils.GetAudioClip("BDArmory/Sounds/deployClick"); + dt = Time.realtimeSinceStartup - tic; + Debug.Log($"DEBUG GetAudioClip took {dt / N:G3}s"); + } + + public static void TestProjectOnPlaneAndPredictPosition() + { + var watch = new System.Diagnostics.Stopwatch(); + float µsResolution = 1e6f / System.Diagnostics.Stopwatch.Frequency; + Debug.Log($"DEBUG Clock resolution: {µsResolution}µs, {PROF_N} outer loops, {PROF_n} inner loops"); + Vessel vessel = FlightGlobals.ActiveVessel; + Vector3 p = vessel.CoM, v = vessel.srf_velocity, a = vessel.acceleration; + Vector3 result = default; + float time = 5f; + var upNormal = VectorUtils.GetUpDirection(p); + + var func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => { for (int i = 0; i < PROF_n; ++i) { result = AIUtils.PredictPosition(p, v, a, time); } }; + Debug.Log($"DEBUG AIUtils.PredictPosition(p, v, a, time) took {ProfileFunc(func, PROF_N) / PROF_n:G3}µs to give {(Vector3d)result}"); + + func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => { for (int i = 0; i < PROF_n; ++i) { result = PredictPositionNoInline(p, v, a, time); } }; + Debug.Log($"DEBUG PredictPositionNoInline(p, v, a, time) took {ProfileFunc(func, PROF_N) / PROF_n:G3}µs to give {(Vector3d)result}"); + + func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => { for (int i = 0; i < PROF_n; ++i) { result = p + time * v + 0.5f * time * time * a; } }; + Debug.Log($"DEBUG p + time * v + 0.5f * time * time * a took {ProfileFunc(func, PROF_N) / PROF_n:G3}µs to give {(Vector3d)result}"); + + func = [MethodImpl(MethodImplOptions.NoInlining)] () => { for (int i = 0; i < PROF_n; ++i) { result = p + time * v + 0.5f * time * time * a; } }; + Debug.Log($"DEBUG p + time * v + 0.5f * time * time * a no-inlining took {ProfileFunc(func, PROF_N) / PROF_n:G3}µs to give {(Vector3d)result}"); + + watch.Reset(); watch.Start(); + for (int i = 0; i < PROF_N * PROF_n; ++i) { result = p + time * v + 0.5f * time * time * a; } + watch.Stop(); + Debug.Log($"DEBUG fully inlined took {watch.ElapsedTicks * µsResolution / PROF_N / PROF_n:G3}µs to give {(Vector3d)result}"); + + func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => { for (int i = 0; i < PROF_n; ++i) { result = Vector3.ProjectOnPlane(v, upNormal); } }; + Debug.Log($"DEBUG Vector3.ProjectOnPlane(v, upNormal) took {ProfileFunc(func, PROF_N) / PROF_n:G3}µs to give {(Vector3d)result}"); + + func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => { for (int i = 0; i < PROF_n; ++i) { result = v.ProjectOnPlane(upNormal); } }; + Debug.Log($"DEBUG v.ProjectOnPlane(upNormal) took {ProfileFunc(func, PROF_N) / PROF_n:G3}µs to give {(Vector3d)result}"); + + func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => { for (int i = 0; i < PROF_n; ++i) { result = ProjectOnPlaneOpt(v, upNormal); } }; + Debug.Log($"DEBUG ProjectOnPlaneOpt(v, upNormal) took {ProfileFunc(func, PROF_N) / PROF_n:G3}µs to give {(Vector3d)result}"); + + func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => { for (int i = 0; i < PROF_n; ++i) { result = v.ProjectOnPlanePreNormalized(upNormal); } }; + Debug.Log($"DEBUG v.ProjectOnPlanePreNormalized(upNormal) took {ProfileFunc(func, PROF_N) / PROF_n:G3}µs to give {(Vector3d)result}"); + + func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => { for (int i = 0; i < PROF_n; ++i) { result = ProjectOnPlanePreNorm(v, upNormal); } }; + Debug.Log($"DEBUG ProjectOnPlanePreNorm(v, upNormal) took {ProfileFunc(func, PROF_N) / PROF_n:G3}µs to give {(Vector3d)result}"); + + func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => { for (int i = 0; i < PROF_n; ++i) { result = ProjectOnPlanePreNormNoInline(v, upNormal); } }; + Debug.Log($"DEBUG ProjectOnPlanePreNormNoInline(v, upNormal) took {ProfileFunc(func, PROF_N) / PROF_n:G3}µs to give {(Vector3d)result}"); + + func = [MethodImpl(MethodImplOptions.AggressiveInlining)] () => { for (int i = 0; i < PROF_n; ++i) { result = v - upNormal * Vector3.Dot(v, upNormal); } }; + Debug.Log($"DEBUG v - upNormal * Vector3.Dot(v, upNormal) took {ProfileFunc(func, PROF_N) / PROF_n:G3}µs to give {(Vector3d)result}"); + + watch.Reset(); watch.Start(); + for (int i = 0; i < PROF_N * PROF_n; ++i) { result = v - upNormal * Vector3.Dot(v, upNormal); } + watch.Stop(); + Debug.Log($"DEBUG fully inlined took {watch.ElapsedTicks * µsResolution / PROF_N / PROF_n:G3}µs to give {(Vector3d)result}"); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static float ProfileFunc(Action func, int N) + { + var watch = new System.Diagnostics.Stopwatch(); + float µsResolution = 1e6f / System.Diagnostics.Stopwatch.Frequency; + func(); // Warm-up + watch.Start(); + for (int i = 0; i < N; ++i) func(); + watch.Stop(); + return watch.ElapsedTicks * µsResolution / N; + } + + public static Vector3 PredictPositionNoInline(Vector3 position, Vector3 velocity, Vector3 acceleration, float time) + { + return position + time * velocity + 0.5f * time * time * acceleration; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 ProjectOnPlaneOpt(Vector3 vector, Vector3 planeNormal) + { + float sqrMag = Vector3.Dot(planeNormal, planeNormal); + if (sqrMag < Mathf.Epsilon) + return vector; + else + { + var dotNorm = Vector3.Dot(vector, planeNormal) / sqrMag; + return new Vector3(vector.x - planeNormal.x * dotNorm, + vector.y - planeNormal.y * dotNorm, + vector.z - planeNormal.z * dotNorm); + } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 ProjectOnPlanePreNorm(Vector3 vector, Vector3 planeNormal) + { + var dot = Vector3.Dot(vector, planeNormal); + return new Vector3(vector.x - planeNormal.x * dot, + vector.y - planeNormal.y * dot, + vector.z - planeNormal.z * dot); + } + public static Vector3 ProjectOnPlanePreNormNoInline(Vector3 vector, Vector3 planeNormal) + { + var dot = Vector3.Dot(vector, planeNormal); + return new Vector3(vector.x - planeNormal.x * dot, + vector.y - planeNormal.y * dot, + vector.z - planeNormal.z * dot); + } #endif } } diff --git a/BDArmory/UI/BDColorPicker.cs b/BDArmory/UI/BDColorPicker.cs index 9ca8eb1fb..75391372d 100644 --- a/BDArmory/UI/BDColorPicker.cs +++ b/BDArmory/UI/BDColorPicker.cs @@ -1,131 +1,127 @@ using KSP.Localization; using UnityEngine; +using BDArmory.Utils; // credit to Brian Jones (https://github.com/boj)& KSP ForumMember TaxiService namespace BDArmory.UI { - [KSPAddon(KSPAddon.Startup.Flight, false)] - public class TeamColorConfig : MonoBehaviour - { - private Texture2D displayPicker; - public int displayTextureWidth = 360; - public int displayTextureHeight = 360; - - public int HorizPos; - public int VertPos; - - public Color selectedColor; - private Texture2D selectedColorPreview; - - private float hueSlider = 0f; - private float prevHueSlider = 0f; - private Texture2D hueTexture; - - protected void Awake() - { - HorizPos = (Screen.width / 2) - (displayTextureWidth / 2); - VertPos = (Screen.height / 2) - (displayTextureHeight / 2); - - renderColorPicker(); - - hueTexture = new Texture2D(10, displayTextureHeight, TextureFormat.ARGB32, false); - for (int x = 0; x < hueTexture.width; x++) - { - for (int y = 0; y < hueTexture.height; y++) - { - float h = (y / (hueTexture.height * 1.0f)) * 1f; - hueTexture.SetPixel(x, y, new ColorHSV(h, 1f, 1f).ToColor()); - } - } - hueTexture.Apply(); - - selectedColorPreview = new Texture2D(1, 1); - selectedColorPreview.SetPixel(0, 0, selectedColor); - } - - private void renderColorPicker() - { - Texture2D colorPicker = new Texture2D(displayTextureWidth, displayTextureHeight, TextureFormat.ARGB32, false); - for (int x = 0; x < displayTextureWidth; x++) - { - for (int y = 0; y < displayTextureHeight; y++) - { - float h = hueSlider; - float v = (y / (displayTextureHeight * 1.0f)) * 1f; - float s = (x / (displayTextureWidth * 1.0f)) * 1f; - colorPicker.SetPixel(x, y, new ColorHSV(h, s, v).ToColor()); - } - } - - colorPicker.Apply(); - displayPicker = colorPicker; - } - - protected void OnGUI() - { - if (!BDTISetup.Instance.showColorSelect) return; - - GUI.Box(new Rect(HorizPos - 3, VertPos - 3, displayTextureWidth + 60, displayTextureHeight + 60), ""); - - if (hueSlider != prevHueSlider) // new Hue value - { - prevHueSlider = hueSlider; - renderColorPicker(); - } - - if (GUI.RepeatButton(new Rect(HorizPos, VertPos, displayTextureWidth, displayTextureHeight), displayPicker)) - { - int a = (int)Input.mousePosition.x; - int b = Screen.height - (int)Input.mousePosition.y; - - selectedColor = displayPicker.GetPixel(a - HorizPos, -(b - VertPos)); - } - - hueSlider = GUI.VerticalSlider(new Rect(HorizPos + displayTextureWidth + 3, VertPos, 10, displayTextureHeight), hueSlider, 1, 0); - GUI.Box(new Rect(HorizPos + displayTextureWidth + 20, VertPos, 20, displayTextureHeight), hueTexture); - - if (GUI.Button(new Rect(HorizPos + displayTextureWidth - 60, VertPos + displayTextureHeight + 10, 60, 25), Localizer.Format("#LOC_BDArmory_Icon_colorget"))) - { - selectedColor = selectedColorPreview.GetPixel(0, 0); - BDTISetup.Instance.showColorSelect = false; - BDTISetup.Instance.UpdateTeamColor = true; - } - - // box for chosen color - GUIStyle style = new GUIStyle(); - selectedColorPreview.SetPixel(0, 0, selectedColor); - selectedColorPreview.Apply(); - style.normal.background = selectedColorPreview; - GUI.Box(new Rect(HorizPos + displayTextureWidth + 10, VertPos + displayTextureHeight + 10, 30, 30), new GUIContent(""), style); - } - float updateTimer; - - void Update() - { - if (!HighLogic.LoadedSceneIsFlight) - { - return; - } - if (BDTISetup.Instance.UpdateTeamColor) - { - updateTimer -= Time.fixedDeltaTime; - { - if (BDTISetup.Instance.UpdateTeamColor && updateTimer < 0) - { - updateTimer = 0.5f; //next update in half a sec only - - if (BDTISetup.Instance.ColorAssignments.ContainsKey(BDTISetup.Instance.selectedTeam)) - { - BDTISetup.Instance.ColorAssignments[BDTISetup.Instance.selectedTeam] = selectedColor; - } - else - { - Debug.Log("[TEAMICONS] Selected team is null."); - } - BDTISetup.Instance.UpdateTeamColor = !BDTISetup.Instance.UpdateTeamColor; - } - } - } - } - } + [KSPAddon(KSPAddon.Startup.Flight, false)] + public class TeamColorConfig : MonoBehaviour + { + private Texture2D displayPicker; + public int displayTextureWidth = 360; + public int displayTextureHeight = 360; + + public int HorizPos; + public int VertPos; + + public Color selectedColor; + private Texture2D selectedColorPreview; + + private float hueSlider = 0f; + private float prevHueSlider = 0f; + private Texture2D hueTexture; + + protected void Awake() + { + HorizPos = (Screen.width / 2) - (displayTextureWidth / 2); + VertPos = (Screen.height / 2) - (displayTextureHeight / 2); + + renderColorPicker(); + + hueTexture = new Texture2D(10, displayTextureHeight, TextureFormat.ARGB32, false); + for (int x = 0; x < hueTexture.width; x++) + { + for (int y = 0; y < hueTexture.height; y++) + { + float h = (y / (hueTexture.height * 1.0f)) * 1f; + hueTexture.SetPixel(x, y, new ColorHSV(h, 1f, 1f).ToColor()); + } + } + hueTexture.Apply(); + + selectedColorPreview = new Texture2D(1, 1); + selectedColorPreview.SetPixel(0, 0, selectedColor); + } + + private void renderColorPicker() + { + Texture2D colorPicker = new Texture2D(displayTextureWidth, displayTextureHeight, TextureFormat.ARGB32, false); + for (int x = 0; x < displayTextureWidth; x++) + { + for (int y = 0; y < displayTextureHeight; y++) + { + float h = hueSlider; + float v = (y / (displayTextureHeight * 1.0f)) * 1f; + float s = (x / (displayTextureWidth * 1.0f)) * 1f; + colorPicker.SetPixel(x, y, new ColorHSV(h, s, v).ToColor()); + } + } + + colorPicker.Apply(); + displayPicker = colorPicker; + } + + protected void OnGUI() + { + if (!BDTISetup.Instance.showColorSelect) return; + + GUI.Box(new Rect(HorizPos - 3, VertPos - 3, displayTextureWidth + 60, displayTextureHeight + 60), ""); + + if (hueSlider != prevHueSlider) // new Hue value + { + prevHueSlider = hueSlider; + renderColorPicker(); + } + + if (GUI.RepeatButton(new Rect(HorizPos, VertPos, displayTextureWidth, displayTextureHeight), displayPicker)) + { + int a = (int)Input.mousePosition.x; + int b = Screen.height - (int)Input.mousePosition.y; + + selectedColor = displayPicker.GetPixel(a - HorizPos, -(b - VertPos)); + } + + hueSlider = GUI.VerticalSlider(new Rect(HorizPos + displayTextureWidth + 3, VertPos, 10, displayTextureHeight), hueSlider, 1, 0); + GUI.Box(new Rect(HorizPos + displayTextureWidth + 20, VertPos, 20, displayTextureHeight), hueTexture); + + if (GUI.Button(new Rect(HorizPos + displayTextureWidth - 60, VertPos + displayTextureHeight + 10, 60, 25), StringUtils.Localize("#LOC_BDArmory_Icon_colorget"))) + { + selectedColor = selectedColorPreview.GetPixel(0, 0); + BDTISetup.Instance.showColorSelect = false; + BDTISetup.Instance.UpdateTeamColor = true; + } + + // box for chosen color + GUIStyle style = new GUIStyle(); + selectedColorPreview.SetPixel(0, 0, selectedColor); + selectedColorPreview.Apply(); + style.normal.background = selectedColorPreview; + GUI.Box(new Rect(HorizPos + displayTextureWidth + 10, VertPos + displayTextureHeight + 10, 30, 30), new GUIContent(""), style); + } + float updateTimer; + + void Update() + { + if (!HighLogic.LoadedSceneIsFlight) return; + if (BDTISetup.Instance.UpdateTeamColor) + { + updateTimer -= Time.deltaTime; + if (updateTimer < 0) + { + updateTimer = 1f; //next update in half a sec only + + if (BDTISetup.Instance.ColorAssignments.ContainsKey(BDTISetup.Instance.selectedTeam)) + { + BDTISetup.Instance.ColorAssignments[BDTISetup.Instance.selectedTeam] = selectedColor; + } + else + { + Debug.Log("[TEAMICONS] Selected team is null."); + } + BDTISetup.Instance.UpdateTeamColor = false; + } + } + } + } } \ No newline at end of file diff --git a/BDArmory/UI/BDGUIComboBox.cs b/BDArmory/UI/BDGUIComboBox.cs index 729501219..9309dfdea 100644 --- a/BDArmory/UI/BDGUIComboBox.cs +++ b/BDArmory/UI/BDGUIComboBox.cs @@ -4,118 +4,112 @@ namespace BDArmory.UI { public class BDGUIComboBox { - private static bool forceToUnShow = false; - private static int useControlID = -1; - private bool isClickedComboButton = false; - public bool isOpen { get { return isClickedComboButton; } } - private int selectedItemIndex = -1; + public bool IsOpen => isOpen; + public float Height => scrollViewRect.height; - private Rect rect; - private Rect buttonRect; - private GUIContent buttonContent; - private GUIContent[] listContent; - private GUIStyle listStyle; - private Vector2 scrollViewVector; - private float comboxbox_height; - private bool thisTriggeredForceClose = false; - private bool forceCloseNow = false; + Rect buttonRect; + Rect listRect; + GUIContent buttonContent; + GUIContent[] listContent; + float maxHeight; + GUIStyle listStyle; + int columns; - public BDGUIComboBox(Rect rect, Rect buttonRect, GUIContent buttonContent, GUIContent[] listContent, float combo_height, GUIStyle listStyle) + bool isClickedComboButton = false; + bool isOpen = false; + int selectedItemIndex = -1; + Vector2 scrollViewVector; + Rect scrollViewRect; + Rect scrollViewInnerRect; + Rect selectionGridRect; + RectOffset selectionGridRectOffset = new RectOffset(3, 3, 3, 3); + float listHeight; + float vScrollWidth = BDArmorySetup.BDGuiSkin.verticalScrollbar.fixedWidth + BDArmorySetup.BDGuiSkin.verticalScrollbar.margin.left; + + /// + /// A drop-down combo-box. + /// + /// The rect for the button. + /// The rect defining the position and width of the selection grid. The height will be adjusted according to the contents. + /// The button content. + /// The selection grid contents. + /// The maximum height of the grid before scrolling is enabled. + /// The GUIStyle to use for the selection grid. + /// The number of columns in the selection grid. + public BDGUIComboBox(Rect buttonRect, Rect listRect, GUIContent buttonContent, GUIContent[] listContent, float maxHeight, GUIStyle listStyle, int columns = 2) { - this.rect = rect; this.buttonRect = buttonRect; + this.listRect = listRect; this.buttonContent = buttonContent; - this.listContent = listContent; - this.listStyle = listStyle; + this.listStyle = new GUIStyle(listStyle); this.listStyle.active.textColor = Color.black; this.listStyle.hover.textColor = Color.black; - this.comboxbox_height = combo_height; + this.maxHeight = maxHeight; + this.columns = columns; + UpdateContent(listContent); } + /// + /// Display the button and combo-box. + /// + /// The selected item's index. public int Show() { - bool done = false; - int controlID = GUIUtility.GetControlID(FocusType.Passive); - - if (forceToUnShow && !thisTriggeredForceClose) // Close all other comboboxes - { isClickedComboButton = false; } - if (forceCloseNow) - { - forceToUnShow = false; - forceCloseNow = false; - } - if (thisTriggeredForceClose) - { - thisTriggeredForceClose = false; - forceCloseNow = true; - } - switch (Event.current.GetTypeForControl(controlID)) + if (GUI.Button(buttonRect, buttonContent, BDArmorySetup.BDGuiSkin.button)) // Button { - case EventType.MouseUp: - if (isClickedComboButton) - { done = true; } - break; + isClickedComboButton = !isClickedComboButton; } + isOpen = isClickedComboButton; // Flag indicating if the selection grid open this frame. - if (GUI.Button(buttonRect, buttonContent, BDArmorySetup.BDGuiSkin.button)) + if (isClickedComboButton) // Selection grid { - if (useControlID == -1) - { useControlID = controlID; } - if (useControlID != controlID) - { - if (isClickedComboButton) - { - forceToUnShow = true; - thisTriggeredForceClose = true; - } - useControlID = controlID; - } - isClickedComboButton = true; - } - - if (isClickedComboButton) - { - float items_height = listStyle.CalcHeight(listContent[0], 1.0f) * (listContent.Length + 5); - Rect listRect = new Rect(rect.x + 5, rect.y + listStyle.CalcHeight(listContent[0], 1.0f), rect.width - 20f, items_height); - - scrollViewVector = GUI.BeginScrollView(new Rect(rect.x, rect.y + rect.height, rect.width + 10f, comboxbox_height), scrollViewVector, - new Rect(rect.x, rect.y, rect.width - 10, items_height + rect.height), false, false, BDArmorySetup.BDGuiSkin.horizontalScrollbar, BDArmorySetup.BDGuiSkin.verticalScrollbar); - - GUI.Box(new Rect(rect.x, rect.y, rect.width - 10, items_height + rect.height), "", BDArmorySetup.BDGuiSkin.window); - - if (selectedItemIndex != (selectedItemIndex = GUI.SelectionGrid(listRect, selectedItemIndex, listContent, 2, listStyle))) + scrollViewVector = GUI.BeginScrollView(scrollViewRect, scrollViewVector, scrollViewInnerRect, BDArmorySetup.BDGuiSkin.horizontalScrollbar, BDArmorySetup.BDGuiSkin.verticalScrollbar); + GUI.Box(scrollViewInnerRect, "", BDArmorySetup.BDGuiSkin.box); // Background box in the scroll view. + if (selectedItemIndex != (selectedItemIndex = GUI.SelectionGrid(selectionGridRect, selectedItemIndex, listContent, columns, listStyle))) // If the selection is changed, then update the UI and close the combo-box. { if (selectedItemIndex > -1) buttonContent.text = listContent[selectedItemIndex].text; - done = true; + isClickedComboButton = false; } - GUI.EndScrollView(); } - if (done) - { isClickedComboButton = false; } - return selectedItemIndex; } - public void UpdateRect(Rect r) + /// + /// Update internal rects when the button rect has moved. + /// + /// + public void UpdateRect(Rect updatedButtonRect) { - if (r == rect) return; - buttonRect.x += r.x - rect.x; - buttonRect.y += r.y - rect.y; - rect = r; + if (updatedButtonRect == buttonRect) return; + listRect.x += updatedButtonRect.x - buttonRect.x; + listRect.y += updatedButtonRect.y - buttonRect.y; + buttonRect = updatedButtonRect; + UpdateScrollViewRect(); } - public int SelectedItemIndex + /// + /// Update the content of the combobox and recalculate sizes. + /// + /// + public void UpdateContent(GUIContent[] content) { - get - { - return selectedItemIndex; - } - set - { - selectedItemIndex = value; - } + listContent = content; + var itemHeight = listStyle.CalcHeight(listContent[0], listRect.width / columns) + listStyle.margin.bottom; + listHeight = itemHeight * Mathf.CeilToInt(listContent.Length / (float)columns); + UpdateScrollViewRect(); + } + + /// + /// Update the rects in the scroll view. + /// + void UpdateScrollViewRect() + { + scrollViewRect = new Rect(listRect.x, listRect.y + listRect.height, listRect.width, Mathf.Min(maxHeight, listHeight + selectionGridRectOffset.vertical)); + scrollViewInnerRect = new Rect(0, 0, scrollViewRect.width, listHeight + selectionGridRectOffset.bottom); + if (scrollViewInnerRect.height > scrollViewRect.height) scrollViewInnerRect.width -= vScrollWidth; + selectionGridRect = selectionGridRectOffset.Remove(scrollViewInnerRect); } } } diff --git a/BDArmory/UI/BDGUIUtils.cs b/BDArmory/UI/BDGUIUtils.cs deleted file mode 100644 index 7c163a3b0..000000000 --- a/BDArmory/UI/BDGUIUtils.cs +++ /dev/null @@ -1,202 +0,0 @@ -using UnityEngine; -using Random = UnityEngine.Random; - -using BDArmory.Core; -using BDArmory.Misc; - -namespace BDArmory.UI -{ - public static class BDGUIUtils - { - public static Texture2D pixel; - - public static Camera GetMainCamera() - { - if (HighLogic.LoadedSceneIsFlight) - { - return FlightCamera.fetch.mainCamera; - } - else - { - return Camera.main; - } - } - - public static void DrawTextureOnWorldPos(Vector3 worldPos, Texture texture, Vector2 size, float wobble) - { - Vector3 screenPos = GetMainCamera().WorldToViewportPoint(worldPos); - if (screenPos.z < 0) return; //dont draw if point is behind camera - if (screenPos.x != Mathf.Clamp01(screenPos.x)) return; //dont draw if off screen - if (screenPos.y != Mathf.Clamp01(screenPos.y)) return; - float xPos = screenPos.x * Screen.width - (0.5f * size.x); - float yPos = (1 - screenPos.y) * Screen.height - (0.5f * size.y); - if (wobble > 0) - { - xPos += Random.Range(-wobble / 2, wobble / 2); - yPos += Random.Range(-wobble / 2, wobble / 2); - } - Rect iconRect = new Rect(xPos, yPos, size.x, size.y); - - GUI.DrawTexture(iconRect, texture); - } - - public static bool WorldToGUIPos(Vector3 worldPos, out Vector2 guiPos) - { - Vector3 screenPos = GetMainCamera().WorldToViewportPoint(worldPos); - bool offScreen = false; - if (screenPos.z < 0) offScreen = true; //dont draw if point is behind camera - if (screenPos.x != Mathf.Clamp01(screenPos.x)) offScreen = true; //dont draw if off screen - if (screenPos.y != Mathf.Clamp01(screenPos.y)) offScreen = true; - if (!offScreen) - { - float xPos = screenPos.x * Screen.width; - float yPos = (1 - screenPos.y) * Screen.height; - guiPos = new Vector2(xPos, yPos); - return true; - } - else - { - guiPos = Vector2.zero; - return false; - } - } - - public static void DrawLineBetweenWorldPositions(Vector3 worldPosA, Vector3 worldPosB, float width, Color color) - { - Camera cam = GetMainCamera(); - - if (cam == null) return; - - GUI.matrix = Matrix4x4.identity; - - bool aBehind = false; - - Plane clipPlane = new Plane(cam.transform.forward, cam.transform.position + cam.transform.forward * 0.05f); - - if (Vector3.Dot(cam.transform.forward, worldPosA - cam.transform.position) < 0) - { - Ray ray = new Ray(worldPosB, worldPosA - worldPosB); - float dist; - if (clipPlane.Raycast(ray, out dist)) - { - worldPosA = ray.GetPoint(dist); - } - aBehind = true; - } - if (Vector3.Dot(cam.transform.forward, worldPosB - cam.transform.position) < 0) - { - if (aBehind) return; - - Ray ray = new Ray(worldPosA, worldPosB - worldPosA); - float dist; - if (clipPlane.Raycast(ray, out dist)) - { - worldPosB = ray.GetPoint(dist); - } - } - - Vector3 screenPosA = cam.WorldToViewportPoint(worldPosA); - screenPosA.x = screenPosA.x * Screen.width; - screenPosA.y = (1 - screenPosA.y) * Screen.height; - Vector3 screenPosB = cam.WorldToViewportPoint(worldPosB); - screenPosB.x = screenPosB.x * Screen.width; - screenPosB.y = (1 - screenPosB.y) * Screen.height; - - screenPosA.z = screenPosB.z = 0; - - float angle = Vector2.Angle(Vector3.up, screenPosB - screenPosA); - if (screenPosB.x < screenPosA.x) - { - angle = -angle; - } - - Vector2 vector = screenPosB - screenPosA; - float length = vector.magnitude; - - Rect upRect = new Rect(screenPosA.x - (width / 2), screenPosA.y - length, width, length); - - GUIUtility.RotateAroundPivot(-angle + 180, screenPosA); - DrawRectangle(upRect, color); - GUI.matrix = Matrix4x4.identity; - } - - public static void DrawRectangle(Rect rect, Color color) - { - if (pixel == null) - { - pixel = new Texture2D(1, 1); - } - - Color originalColor = GUI.color; - GUI.color = color; - GUI.DrawTexture(rect, pixel); - GUI.color = originalColor; - } - - public static void MarkPosition(Transform transform, Color color) => MarkPosition(transform.position, transform, color); - - public static void MarkPosition(Vector3 position, Transform transform, Color color, float size = 3, float thickness = 2) - { - DrawLineBetweenWorldPositions(position + transform.right * size, position - transform.right * size, thickness, color); - DrawLineBetweenWorldPositions(position + transform.up * size, position - transform.up * size, thickness, color); - DrawLineBetweenWorldPositions(position + transform.forward * size, position - transform.forward * size, thickness, color); - } - - public static void UseMouseEventInRect(Rect rect) - { - if (Event.current == null) return; - if (Utils.MouseIsInRect(rect) && Event.current.isMouse && (Event.current.type == EventType.MouseDown || Event.current.type == EventType.MouseUp)) - { - Event.current.Use(); - } - } - - public static Rect CleanRectVals(Rect rect) - { - // Remove decimal places so Mac does not complain. - rect.x = (int)rect.x; - rect.y = (int)rect.y; - rect.width = (int)rect.width; - rect.height = (int)rect.height; - return rect; - } - - internal static void RepositionWindow(ref Rect windowPosition) - { - if (BDArmorySettings.STRICT_WINDOW_BOUNDARIES) - { - // This method uses Gui point system. - if (windowPosition.x < 0) windowPosition.x = 0; - if (windowPosition.y < 0) windowPosition.y = 0; - - if (windowPosition.xMax > Screen.width) // Don't go off the right of the screen. - windowPosition.x = Screen.width - windowPosition.width; - if (windowPosition.height > Screen.height) // Don't go off the top of the screen. - windowPosition.y = 0; - else if (windowPosition.yMax > Screen.height) // Don't go off the bottom of the screen. - windowPosition.y = Screen.height - windowPosition.height; - } - else // If the window is completely off-screen, bring it just onto the screen. - { - if (windowPosition.width == 0) windowPosition.width = 1; - if (windowPosition.height == 0) windowPosition.height = 1; - if (windowPosition.x >= Screen.width) windowPosition.x = Screen.width - 1; - if (windowPosition.y >= Screen.height) windowPosition.y = Screen.height - 1; - if (windowPosition.x + windowPosition.width < 1) windowPosition.x = 1 - windowPosition.width; - if (windowPosition.y + windowPosition.height < 1) windowPosition.y = 1 - windowPosition.height; - } - } - - internal static Rect GuiToScreenRect(Rect rect) - { - // Must run during OnGui to work... - Rect newRect = new Rect - { - position = GUIUtility.GUIToScreenPoint(rect.position), - width = rect.width, - height = rect.height - }; - return newRect; - } - } -} diff --git a/BDArmory/UI/BDStagingAreaGauge.cs b/BDArmory/UI/BDStagingAreaGauge.cs index 82a28445b..db9929466 100644 --- a/BDArmory/UI/BDStagingAreaGauge.cs +++ b/BDArmory/UI/BDStagingAreaGauge.cs @@ -1,16 +1,14 @@ -using BDArmory.Core; +using KSP.Localization; using KSP.UI.Screens; using UnityEngine; -using KSP.Localization; + +using BDArmory.Settings; +using BDArmory.Utils; namespace BDArmory.UI { public class BDStagingAreaGauge : PartModule { - public AudioSource AudioSource; - public AudioClip ReloadAudioClip = null; - public AudioClip ReloadCompleteAudioClip = null; - public string AmmoName = ""; //UI gauges(next to staging icon) @@ -101,20 +99,12 @@ public void UpdateReloadMeter(float reloadRemaining) if (reloadBar == null) { reloadBar = InitReloadBar(); - if (ReloadAudioClip) - { - AudioSource.PlayOneShot(ReloadAudioClip); - } } reloadBar?.SetValue(reloadRemaining, 0, 1); } else if (reloadBar != null) { ForceRedraw(); - if (ReloadCompleteAudioClip) - { - AudioSource.PlayOneShot(ReloadCompleteAudioClip); - } } } @@ -149,7 +139,7 @@ private ProtoStageIconInfo InitReloadBar() return v; v.SetMsgBgColor(XKCDColors.DarkGrey); v.SetMsgTextColor(XKCDColors.White); - v.SetMessage(Localizer.Format("#LOC_BDArmory_ProtoStageIconInfo_Reloading"));//"Reloading" + v.SetMessage(StringUtils.Localize("#LOC_BDArmory_ProtoStageIconInfo_Reloading"));//"Reloading" v.SetProgressBarBgColor(XKCDColors.DarkGrey); v.SetProgressBarColor(XKCDColors.Silver); @@ -166,7 +156,7 @@ private ProtoStageIconInfo InitHeatGauge() //thanks DYJ { v.SetMsgBgColor(XKCDColors.DarkRed); v.SetMsgTextColor(XKCDColors.Orange); - v.SetMessage(Localizer.Format("#LOC_BDArmory_ProtoStageIconInfo_Overheat"));//"Overheat" + v.SetMessage(StringUtils.Localize("#LOC_BDArmory_ProtoStageIconInfo_Overheat"));//"Overheat" v.SetProgressBarBgColor(XKCDColors.DarkRed); v.SetProgressBarColor(XKCDColors.Orange); } @@ -199,7 +189,7 @@ private ProtoStageIconInfo InitEmptyGauge() //could remove emptygauge, mainly a { g.SetMsgBgColor(XKCDColors.AlmostBlack); g.SetMsgTextColor(XKCDColors.Yellow); - g.SetMessage(Localizer.Format("#LOC_BDArmory_ProtoStageIconInfo_AmmoOut"));//"Ammo Depleted" + g.SetMessage(StringUtils.Localize("#LOC_BDArmory_ProtoStageIconInfo_AmmoOut"));//"Ammo Depleted" g.SetProgressBarBgColor(XKCDColors.Yellow); g.SetProgressBarColor(XKCDColors.Black); } diff --git a/BDArmory/UI/BDTISetup.cs b/BDArmory/UI/BDTISetup.cs index 219254655..0c95b248a 100644 --- a/BDArmory/UI/BDTISetup.cs +++ b/BDArmory/UI/BDTISetup.cs @@ -1,12 +1,15 @@ -using System; -using System.Collections; -using UnityEngine; +using KSP.Localization; using KSP.UI.Screens; using System.Collections.Generic; -using BDArmory.Modules; -using BDArmory.Misc; +using System.Collections; using System.Linq; -using KSP.Localization; +using System; +using UnityEngine; + +using BDArmory.Competition; +using BDArmory.Control; +using BDArmory.Settings; +using BDArmory.Utils; /* * *Milestone 6: Figure out how to have TI activation toggle the F4 SHOW_LABELS (or is it Flt_Show_labels?) method to sim a keypress? @@ -19,7 +22,7 @@ class BDTISetup : MonoBehaviour private ApplicationLauncherButton toolbarButton = null; public static Rect WindowRectGUI; - private string windowTitle = Localizer.Format("#LOC_BDArmory_Icons_title"); + private string windowTitle = StringUtils.Localize("#LOC_BDArmory_Icons_title"); public static BDTISetup Instance = null; public static GUIStyle TILabel; private bool showTeamIconGUI = false; @@ -191,7 +194,7 @@ void Start() { if (LegacyTILoaded) { - ScreenMessages.PostScreenMessage(Localizer.Format("#LOC_BDArmory_Icons_legacyinstall"), 20.0f, ScreenMessageStyle.UPPER_CENTER); + ScreenMessages.PostScreenMessage(StringUtils.Localize("#LOC_BDArmory_Icons_legacyinstall"), 20.0f, ScreenMessageStyle.UPPER_CENTER); } } TILabel = new GUIStyle(); @@ -220,11 +223,11 @@ private void Update() { if (BDTISettings.TEAMICONS) { - updateList -= Time.fixedDeltaTime; + updateList -= Time.deltaTime; if (updateList < 0) { UpdateList(); - updateList = 0.5f; // check team lists less often than every frame + updateList = 1f; // check team lists less often than every frame } } } @@ -254,7 +257,18 @@ private void UpdateList(bool fromModifiedEvent = false) } } } - + private void ResetColors() + { + ColorAssignments.Clear(); + UpdateList(); + int colorcount = 0; + var teams = ColorAssignments.Keys.ToList(); + float teamsCount = (float)teams.Count; + foreach (var team in teams) + { + ColorAssignments[team] = Color.HSVToRGB(++colorcount / teamsCount, 1f, 1f); + } + } private void OnDestroy() { if (toolbarButton) @@ -271,10 +285,7 @@ private void OnDestroy() IEnumerator ToolbarButtonRoutine() { if (toolbarButton || (!HighLogic.LoadedSceneIsEditor)) yield break; - while (!ApplicationLauncher.Ready) - { - yield return null; - } + yield return new WaitUntil(() => ApplicationLauncher.Ready && BDArmorySetup.toolbarButtonAdded); // Wait until after the main BDA toolbar button. AddToolbarButton(); } @@ -294,7 +305,7 @@ public void ShowToolbarGUI() { if (LegacyTILoaded) { - ScreenMessages.PostScreenMessage(Localizer.Format("#LOC_BDArmory_Icons_legacyinstall"), 5.0f, ScreenMessageStyle.UPPER_CENTER); + ScreenMessages.PostScreenMessage(StringUtils.Localize("#LOC_BDArmory_Icons_legacyinstall"), 5.0f, ScreenMessageStyle.UPPER_CENTER); } else { @@ -350,7 +361,7 @@ void OnGUI() maySavethisInstance = true; } WindowRectGUI = new Rect(Screen.width - toolWindowWidth - 40, 150, toolWindowWidth, toolWindowHeight); - WindowRectGUI = GUI.Window(this.GetInstanceID(), WindowRectGUI, TeamIconGUI, windowTitle, BDArmorySetup.BDGuiSkin.window); + WindowRectGUI = GUI.Window(GUIUtility.GetControlID(FocusType.Passive), WindowRectGUI, TeamIconGUI, windowTitle, BDArmorySetup.BDGuiSkin.window); } title = new GUIStyle(GUI.skin.label); title.fontSize = 30; @@ -360,7 +371,7 @@ void OnGUI() { if (GameSettings.FLT_VESSEL_LABELS && !showPSA) { - ScreenMessages.PostScreenMessage(Localizer.Format("#LOC_BDArmory_Icons_PSA"), 20.0f, ScreenMessageStyle.UPPER_CENTER); + ScreenMessages.PostScreenMessage(StringUtils.Localize("#LOC_BDArmory_Icons_PSA"), 20.0f, ScreenMessageStyle.UPPER_CENTER); showPSA = true; } } @@ -371,31 +382,31 @@ void OnGUI() void TeamIconGUI(int windowID) { float line = 0; - BDTISettings.TEAMICONS = GUI.Toggle(new Rect(5, 25, toolWindowWidth, 20), BDTISettings.TEAMICONS, Localizer.Format("#LOC_BDArmory_Enable_Icons"), BDArmorySetup.BDGuiSkin.toggle); + BDTISettings.TEAMICONS = GUI.Toggle(new Rect(5, 25, toolWindowWidth, 20), BDTISettings.TEAMICONS, StringUtils.Localize("#LOC_BDArmory_Enable_Icons"), BDArmorySetup.BDGuiSkin.toggle); if (BDTISettings.TEAMICONS) { if (GameSettings.FLT_VESSEL_LABELS && !showPSA) { - ScreenMessages.PostScreenMessage(Localizer.Format("#LOC_BDArmory_Icons_PSA"), 7.0f, ScreenMessageStyle.UPPER_CENTER); + ScreenMessages.PostScreenMessage(StringUtils.Localize("#LOC_BDArmory_Icons_PSA"), 7.0f, ScreenMessageStyle.UPPER_CENTER); showPSA = true; } GUI.BeginGroup(IconOptionsGroup, GUIContent.none, BDArmorySetup.BDGuiSkin.box); - BDTISettings.TEAMNAMES = GUI.Toggle(new Rect(15, line++ * 25, toolWindowWidth - 20, 20), BDTISettings.TEAMNAMES, Localizer.Format("#LOC_BDArmory_Icon_teams"), BDArmorySetup.BDGuiSkin.toggle); - BDTISettings.VESSELNAMES = GUI.Toggle(new Rect(15, line++ * 25, toolWindowWidth - 20, 20), BDTISettings.VESSELNAMES, Localizer.Format("#LOC_BDArmory_Icon_names"), BDArmorySetup.BDGuiSkin.toggle); - BDTISettings.SCORE = GUI.Toggle(new Rect(15, line++ * 25, toolWindowWidth - 20, 20), BDTISettings.SCORE, Localizer.Format("#LOC_BDArmory_Icon_score"), BDArmorySetup.BDGuiSkin.toggle); - BDTISettings.HEALTHBAR = GUI.Toggle(new Rect(15, line++ * 25, toolWindowWidth - 20, 20), BDTISettings.HEALTHBAR, Localizer.Format("#LOC_BDArmory_Icon_healthbars"), BDArmorySetup.BDGuiSkin.toggle); - BDTISettings.SHOW_SELF = GUI.Toggle(new Rect(15, line++ * 25, toolWindowWidth - 20, 20), BDTISettings.SHOW_SELF, Localizer.Format("#LOC_BDArmory_Icon_show_self"), BDArmorySetup.BDGuiSkin.toggle); - BDTISettings.MISSILES = GUI.Toggle(new Rect(15, line++ * 25, toolWindowWidth - 20, 20), BDTISettings.MISSILES, Localizer.Format("#LOC_BDArmory_Icon_missiles"), BDArmorySetup.BDGuiSkin.toggle); - BDTISettings.DEBRIS = GUI.Toggle(new Rect(15, line++ * 25, toolWindowWidth - 20, 20), BDTISettings.DEBRIS, Localizer.Format("#LOC_BDArmory_Icon_debris"), BDArmorySetup.BDGuiSkin.toggle); - BDTISettings.PERSISTANT = GUI.Toggle(new Rect(15, line++ * 25, toolWindowWidth - 20, 20), BDTISettings.PERSISTANT, Localizer.Format("#LOC_BDArmory_Icon_persist"), BDArmorySetup.BDGuiSkin.toggle); - BDTISettings.THREATICON = GUI.Toggle(new Rect(15, line++ * 25, toolWindowWidth - 20, 20), BDTISettings.THREATICON, Localizer.Format("#LOC_BDArmory_Icon_threats"), BDArmorySetup.BDGuiSkin.toggle); - BDTISettings.POINTERS = GUI.Toggle(new Rect(15, line++ * 25, toolWindowWidth - 20, 20), BDTISettings.POINTERS, Localizer.Format("#LOC_BDArmory_Icon_pointers"), BDArmorySetup.BDGuiSkin.toggle); - BDTISettings.TELEMETRY = GUI.Toggle(new Rect(15, line++ * 25, toolWindowWidth - 20, 20), BDTISettings.TELEMETRY, Localizer.Format("#LOC_BDArmory_Icon_telemetry"), BDArmorySetup.BDGuiSkin.toggle); + BDTISettings.TEAMNAMES = GUI.Toggle(new Rect(15, line++ * 25, toolWindowWidth - 20, 20), BDTISettings.TEAMNAMES, StringUtils.Localize("#LOC_BDArmory_Icon_teams"), BDArmorySetup.BDGuiSkin.toggle); + BDTISettings.VESSELNAMES = GUI.Toggle(new Rect(15, line++ * 25, toolWindowWidth - 20, 20), BDTISettings.VESSELNAMES, StringUtils.Localize("#LOC_BDArmory_Icon_names"), BDArmorySetup.BDGuiSkin.toggle); + BDTISettings.SCORE = GUI.Toggle(new Rect(15, line++ * 25, toolWindowWidth - 20, 20), BDTISettings.SCORE, StringUtils.Localize("#LOC_BDArmory_Icon_score"), BDArmorySetup.BDGuiSkin.toggle); + BDTISettings.HEALTHBAR = GUI.Toggle(new Rect(15, line++ * 25, toolWindowWidth - 20, 20), BDTISettings.HEALTHBAR, StringUtils.Localize("#LOC_BDArmory_Icon_healthbars"), BDArmorySetup.BDGuiSkin.toggle); + BDTISettings.SHOW_SELF = GUI.Toggle(new Rect(15, line++ * 25, toolWindowWidth - 20, 20), BDTISettings.SHOW_SELF, StringUtils.Localize("#LOC_BDArmory_Icon_show_self"), BDArmorySetup.BDGuiSkin.toggle); + BDTISettings.MISSILES = GUI.Toggle(new Rect(15, line++ * 25, toolWindowWidth - 20, 20), BDTISettings.MISSILES, StringUtils.Localize("#LOC_BDArmory_Icon_missiles"), BDArmorySetup.BDGuiSkin.toggle); + BDTISettings.DEBRIS = GUI.Toggle(new Rect(15, line++ * 25, toolWindowWidth - 20, 20), BDTISettings.DEBRIS, StringUtils.Localize("#LOC_BDArmory_Icon_debris"), BDArmorySetup.BDGuiSkin.toggle); + BDTISettings.PERSISTANT = GUI.Toggle(new Rect(15, line++ * 25, toolWindowWidth - 20, 20), BDTISettings.PERSISTANT, StringUtils.Localize("#LOC_BDArmory_Icon_persist"), BDArmorySetup.BDGuiSkin.toggle); + BDTISettings.THREATICON = GUI.Toggle(new Rect(15, line++ * 25, toolWindowWidth - 20, 20), BDTISettings.THREATICON, StringUtils.Localize("#LOC_BDArmory_Icon_threats"), BDArmorySetup.BDGuiSkin.toggle); + BDTISettings.POINTERS = GUI.Toggle(new Rect(15, line++ * 25, toolWindowWidth - 20, 20), BDTISettings.POINTERS, StringUtils.Localize("#LOC_BDArmory_Icon_pointers"), BDArmorySetup.BDGuiSkin.toggle); + BDTISettings.TELEMETRY = GUI.Toggle(new Rect(15, line++ * 25, toolWindowWidth - 20, 20), BDTISettings.TELEMETRY, StringUtils.Localize("#LOC_BDArmory_Icon_telemetry"), BDArmorySetup.BDGuiSkin.toggle); line += 0.25f; - GUI.Label(new Rect(15, line++ * 25, toolWindowWidth - 20, 20), Localizer.Format("#LOC_BDArmory_Icon_scale") + " " + (BDTISettings.ICONSCALE * 100f).ToString("0") + "%"); + GUI.Label(new Rect(15, line++ * 25, toolWindowWidth - 20, 20), StringUtils.Localize("#LOC_BDArmory_Icon_scale") + " " + (BDTISettings.ICONSCALE * 100f).ToString("0") + "%"); BDTISettings.ICONSCALE = GUI.HorizontalSlider(new Rect(10, line++ * 25, toolWindowWidth - 40, 20), BDTISettings.ICONSCALE, 0.25f, 2f); line -= 0.15f; - GUI.Label(new Rect(15, line++ * 25, toolWindowWidth - 20, 20), Localizer.Format("#LOC_BDArmory_Icon_distance_threshold") + " " + (BDTISettings.DISTANCE_THRESHOLD).ToString("0") + "m"); + GUI.Label(new Rect(15, line++ * 25, toolWindowWidth - 20, 20), StringUtils.Localize("#LOC_BDArmory_Icon_distance_threshold") + " " + (BDTISettings.DISTANCE_THRESHOLD).ToString("0") + "m"); BDTISettings.DISTANCE_THRESHOLD = Mathf.Round(GUI.HorizontalSlider(new Rect(10, line++ * 25, toolWindowWidth - 40, 20), BDTISettings.DISTANCE_THRESHOLD, 10f, 250f) / 10f) * 10f; GUI.EndGroup(); IconOptionsGroup.height = 25f * line; @@ -420,6 +431,12 @@ void TeamIconGUI(int windowID) } GUI.Label(new Rect(5, -20 + (line * 25), 25, 25), "*", title); } + line++; + Rect resetRect = new Rect(30, -20 + (line * 25), 190, 20); + if (GUI.Button(resetRect, "Reset TeamColors")) + { + ResetColors(); + } GUI.EndGroup(); TeamColorsGroup.height = Mathf.Lerp(TeamColorsGroup.height, (line * 25) + 5, 0.35f); } diff --git a/BDArmory/UI/BDTargetSelector.cs b/BDArmory/UI/BDTargetSelector.cs index 547ea813b..67b8c33dc 100644 --- a/BDArmory/UI/BDTargetSelector.cs +++ b/BDArmory/UI/BDTargetSelector.cs @@ -1,10 +1,10 @@ using System.Collections; -using BDArmory.Core; -using BDArmory.Misc; -using BDArmory.Modules; using UnityEngine; using KSP.Localization; +using BDArmory.Control; +using BDArmory.Utils; + namespace BDArmory.UI { [KSPAddon(KSPAddon.Startup.FlightAndEditor, false)] @@ -17,7 +17,7 @@ public class BDTargetSelector : MonoBehaviour const float buttonHeight = 20; const float buttonGap = 2; - private int guiCheckIndex; + private static int guiCheckIndex = -1; private bool ready = false; private bool open = false; private Rect window; @@ -28,39 +28,46 @@ public class BDTargetSelector : MonoBehaviour public void Open(MissileFire weaponManager, Vector2 position) { - open = true; + SetVisible(true); targetWeaponManager = weaponManager; windowLocation = position; } + void SetVisible(bool visible) + { + open = visible; + GUIUtils.SetGUIRectVisible(guiCheckIndex, visible); + } + private void TargetingSelectorWindow(int id) { height = margin; GUIStyle labelStyle = BDArmorySetup.BDGuiSkin.label; - GUI.Label(new Rect(margin, height, width - 2 * margin, buttonHeight), Localizer.Format("#LOC_BDArmory_Selecttargeting"), labelStyle); + GUI.Label(new Rect(margin, height, width - 2 * margin, buttonHeight), StringUtils.Localize("#LOC_BDArmory_Selecttargeting"), labelStyle); if (GUI.Button(new Rect(width - 18, 2, 16, 16), "X")) { - open = false; + SetVisible(false); } height += buttonHeight; - + height += buttonGap; Rect CoMRect = new Rect(margin, height, width - 2 * margin, buttonHeight); GUIStyle CoMStyle = targetWeaponManager.targetCoM ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button; //FIXME - switch these over to toggles instead of buttons; identified issue with weapon/engine targeting no sawing? - if (GUI.Button(CoMRect, Localizer.Format("#LOC_BDArmory_TargetCOM"), CoMStyle)) + if (GUI.Button(CoMRect, StringUtils.Localize("#LOC_BDArmory_TargetCOM"), CoMStyle)) { targetWeaponManager.targetCoM = !targetWeaponManager.targetCoM; if (targetWeaponManager.targetCoM) { - targetWeaponManager.targetWeapon = false; - targetWeaponManager.targetEngine = false; targetWeaponManager.targetCommand = false; + targetWeaponManager.targetEngine = false; + targetWeaponManager.targetWeapon = false; targetWeaponManager.targetMass = false; + targetWeaponManager.targetRandom = false; } - if (!targetWeaponManager.targetCoM && (!targetWeaponManager.targetWeapon && !targetWeaponManager.targetEngine && !targetWeaponManager.targetCommand && !targetWeaponManager.targetMass)) + if (!targetWeaponManager.targetCoM && (!targetWeaponManager.targetWeapon && !targetWeaponManager.targetEngine && !targetWeaponManager.targetCommand && !targetWeaponManager.targetMass && !targetWeaponManager.targetRandom)) { - targetWeaponManager.targetMass = true; + targetWeaponManager.targetRandom = true; } } height += buttonHeight; @@ -69,14 +76,14 @@ private void TargetingSelectorWindow(int id) Rect MassRect = new Rect(margin, height, width - 2 * margin, buttonHeight); GUIStyle MassStyle = targetWeaponManager.targetMass ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button; - if (GUI.Button(MassRect, Localizer.Format("#LOC_BDArmory_Mass"), MassStyle)) + if (GUI.Button(MassRect, StringUtils.Localize("#LOC_BDArmory_Mass"), MassStyle)) { targetWeaponManager.targetMass = !targetWeaponManager.targetMass; if (targetWeaponManager.targetMass) { targetWeaponManager.targetCoM = false; } - if (!targetWeaponManager.targetCoM && (!targetWeaponManager.targetWeapon && !targetWeaponManager.targetEngine && !targetWeaponManager.targetCommand && !targetWeaponManager.targetMass)) + if (!targetWeaponManager.targetCoM && (!targetWeaponManager.targetWeapon && !targetWeaponManager.targetEngine && !targetWeaponManager.targetCommand && !targetWeaponManager.targetMass && !targetWeaponManager.targetRandom)) { targetWeaponManager.targetCoM = true; } @@ -87,14 +94,14 @@ private void TargetingSelectorWindow(int id) Rect CommandRect = new Rect(margin, height, width - 2 * margin, buttonHeight); GUIStyle CommandStyle = targetWeaponManager.targetCommand ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button; - if (GUI.Button(CommandRect, Localizer.Format("#LOC_BDArmory_Command"), CommandStyle)) + if (GUI.Button(CommandRect, StringUtils.Localize("#LOC_BDArmory_Command"), CommandStyle)) { targetWeaponManager.targetCommand = !targetWeaponManager.targetCommand; if (targetWeaponManager.targetCommand) { targetWeaponManager.targetCoM = false; } - if (!targetWeaponManager.targetCoM && (!targetWeaponManager.targetWeapon && !targetWeaponManager.targetEngine && !targetWeaponManager.targetCommand && !targetWeaponManager.targetMass)) + if (!targetWeaponManager.targetCoM && (!targetWeaponManager.targetWeapon && !targetWeaponManager.targetEngine && !targetWeaponManager.targetCommand && !targetWeaponManager.targetMass && !targetWeaponManager.targetRandom)) { targetWeaponManager.targetCoM = true; } @@ -105,14 +112,14 @@ private void TargetingSelectorWindow(int id) Rect EngineRect = new Rect(margin, height, width - 2 * margin, buttonHeight); GUIStyle EngineStyle = targetWeaponManager.targetEngine ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button; - if (GUI.Button(EngineRect, Localizer.Format("#LOC_BDArmory_Engines"), EngineStyle)) + if (GUI.Button(EngineRect, StringUtils.Localize("#LOC_BDArmory_Engines"), EngineStyle)) { targetWeaponManager.targetEngine = !targetWeaponManager.targetEngine; if (targetWeaponManager.targetEngine) { targetWeaponManager.targetCoM = false; } - if (!targetWeaponManager.targetCoM && (!targetWeaponManager.targetWeapon && !targetWeaponManager.targetEngine && !targetWeaponManager.targetCommand && !targetWeaponManager.targetMass)) + if (!targetWeaponManager.targetCoM && (!targetWeaponManager.targetWeapon && !targetWeaponManager.targetEngine && !targetWeaponManager.targetCommand && !targetWeaponManager.targetMass && !targetWeaponManager.targetRandom)) { targetWeaponManager.targetCoM = true; } @@ -123,14 +130,32 @@ private void TargetingSelectorWindow(int id) Rect weaponRect = new Rect(margin, height, width - 2 * margin, buttonHeight); GUIStyle WepStyle = targetWeaponManager.targetWeapon ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button; - if (GUI.Button(weaponRect, Localizer.Format("#LOC_BDArmory_Weapons"), WepStyle)) + if (GUI.Button(weaponRect, StringUtils.Localize("#LOC_BDArmory_Weapons"), WepStyle)) { targetWeaponManager.targetWeapon = !targetWeaponManager.targetWeapon; if (targetWeaponManager.targetWeapon) { targetWeaponManager.targetCoM = false; } - if (!targetWeaponManager.targetCoM && (!targetWeaponManager.targetWeapon && !targetWeaponManager.targetEngine && !targetWeaponManager.targetCommand && !targetWeaponManager.targetMass)) + if (!targetWeaponManager.targetCoM && (!targetWeaponManager.targetWeapon && !targetWeaponManager.targetEngine && !targetWeaponManager.targetCommand && !targetWeaponManager.targetMass && !targetWeaponManager.targetRandom)) + { + targetWeaponManager.targetCoM = true; + } + } + height += buttonHeight; + + height += buttonGap; + Rect RNGRect = new Rect(margin, height, width - 2 * margin, buttonHeight); + GUIStyle RNGStyle = targetWeaponManager.targetWeapon ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button; + + if (GUI.Button(RNGRect, StringUtils.Localize("#LOC_BDArmory_Random"), RNGStyle)) + { + targetWeaponManager.targetRandom = !targetWeaponManager.targetRandom; + if (targetWeaponManager.targetRandom) + { + targetWeaponManager.targetCoM = false; + } + if (!targetWeaponManager.targetCoM && (!targetWeaponManager.targetWeapon && !targetWeaponManager.targetEngine && !targetWeaponManager.targetCommand && !targetWeaponManager.targetMass && !targetWeaponManager.targetRandom)) { targetWeaponManager.targetCoM = true; } @@ -138,13 +163,14 @@ private void TargetingSelectorWindow(int id) height += buttonHeight; height += margin; - targetWeaponManager.targetingString = (targetWeaponManager.targetCoM ? Localizer.Format("#LOC_BDArmory_TargetCOM") + "; " : "") - + (targetWeaponManager.targetMass ? Localizer.Format("#LOC_BDArmory_Mass") + "; " : "") - + (targetWeaponManager.targetCommand ? Localizer.Format("#LOC_BDArmory_Command") + "; " : "") - + (targetWeaponManager.targetEngine ? Localizer.Format("#LOC_BDArmory_Engines") + "; " : "") - + (targetWeaponManager.targetWeapon ? Localizer.Format("#LOC_BDArmory_Weapons") + "; " : ""); - BDGUIUtils.RepositionWindow(ref window); - BDGUIUtils.UseMouseEventInRect(window); + targetWeaponManager.targetingString = (targetWeaponManager.targetCoM ? StringUtils.Localize("#LOC_BDArmory_TargetCOM") + "; " : "") + + (targetWeaponManager.targetMass ? StringUtils.Localize("#LOC_BDArmory_Mass") + "; " : "") + + (targetWeaponManager.targetCommand ? StringUtils.Localize("#LOC_BDArmory_Command") + "; " : "") + + (targetWeaponManager.targetEngine ? StringUtils.Localize("#LOC_BDArmory_Engines") + "; " : "") + + (targetWeaponManager.targetWeapon ? StringUtils.Localize("#LOC_BDArmory_Weapons") + "; " : "") + +(targetWeaponManager.targetRandom ? StringUtils.Localize("#LOC_BDArmory_Random") + "; " : ""); + GUIUtils.RepositionWindow(ref window); + GUIUtils.UseMouseEventInRect(window); } protected virtual void OnGUI() @@ -154,13 +180,15 @@ protected virtual void OnGUI() { if (!open) return; - var clientRect = new Rect( - Mathf.Min(windowLocation.x, Screen.width - width), - Mathf.Min(windowLocation.y, Screen.height - height), - width, - height); - window = GUI.Window(10591029, clientRect, TargetingSelectorWindow, "", BDArmorySetup.BDGuiSkin.window); - Utils.UpdateGUIRect(window, guiCheckIndex); + var clientRect = new Rect( + Mathf.Min(windowLocation.x, Screen.width - width), + Mathf.Min(windowLocation.y, Screen.height - height), + width, + height); + BDArmorySetup.SetGUIOpacity(); + window = GUI.Window(10591029, clientRect, TargetingSelectorWindow, "", BDArmorySetup.BDGuiSkin.window); + BDArmorySetup.SetGUIOpacity(false); + GUIUtils.UpdateGUIRect(window, guiCheckIndex); } } @@ -183,11 +211,10 @@ private void OnDestroy() private IEnumerator WaitForBdaSettings() { - while (BDArmorySetup.Instance == null) - yield return null; + yield return new WaitUntil(() => BDArmorySetup.Instance is not null); ready = true; - guiCheckIndex = Utils.RegisterGUIRect(new Rect()); + if (guiCheckIndex < 0) guiCheckIndex = GUIUtils.RegisterGUIRect(new Rect()); } } } diff --git a/BDArmory/UI/BDTeamSelector.cs b/BDArmory/UI/BDTeamSelector.cs index 0cca10bf3..4018067b9 100644 --- a/BDArmory/UI/BDTeamSelector.cs +++ b/BDArmory/UI/BDTeamSelector.cs @@ -1,10 +1,11 @@ using System.Collections; -using BDArmory.Core; -using BDArmory.Misc; -using BDArmory.Modules; using UnityEngine; using KSP.Localization; +using BDArmory.Competition; +using BDArmory.Control; +using BDArmory.Utils; + namespace BDArmory.UI { [KSPAddon(KSPAddon.Startup.FlightAndEditor, false)] @@ -19,7 +20,7 @@ public class BDTeamSelector : MonoBehaviour const float newTeanButtonWidth = 40; const float scrollWidth = 20; - private int guiCheckIndex; + private static int guiCheckIndex = -1; private bool ready = false; private bool open = false; private Rect window; @@ -33,12 +34,18 @@ public class BDTeamSelector : MonoBehaviour public void Open(MissileFire weaponManager, Vector2 position) { - open = true; + SetVisible(true); targetWeaponManager = weaponManager; newTeamName = string.Empty; windowLocation = position; } + void SetVisible(bool visible) + { + open = visible; + GUIUtils.SetGUIRectVisible(guiCheckIndex, visible); + } + private void TeamSelectorWindow(int id) { height = margin; @@ -47,12 +54,12 @@ private void TeamSelectorWindow(int id) // New team button Rect newTeamButtonRect = new Rect(width - margin - newTeanButtonWidth, height, newTeanButtonWidth, buttonHeight); - if (GUI.Button(newTeamButtonRect, Localizer.Format("#LOC_BDArmory_Generic_New"), BDArmorySetup.BDGuiSkin.button))//"New" + if (GUI.Button(newTeamButtonRect, StringUtils.Localize("#LOC_BDArmory_Generic_New"), BDArmorySetup.BDGuiSkin.button))//"New" { if (!string.IsNullOrEmpty(newTeamName.Trim())) { targetWeaponManager.SetTeam(BDTeam.Get(newTeamName.Trim())); - open = false; + SetVisible(false); } } @@ -83,11 +90,11 @@ private void TeamSelectorWindow(int id) { case 1: // right click if (teams.Current.Name != "Neutral" && teams.Current.Name != "A" && teams.Current.Name != "B") - teams.Current.Neutral = !teams.Current.Neutral; + teams.Current.Neutral = !teams.Current.Neutral; break; default: targetWeaponManager.SetTeam(teams.Current); - open = false; + SetVisible(false); break; } } @@ -103,17 +110,17 @@ private void TeamSelectorWindow(int id) if ((Event.current.keyCode == KeyCode.Return || Event.current.keyCode == KeyCode.KeypadEnter) && !string.IsNullOrEmpty(newTeamName.Trim())) { targetWeaponManager.SetTeam(BDTeam.Get(newTeamName.Trim())); - open = false; + SetVisible(false); } else if (Event.current.keyCode == KeyCode.Escape) { - open = false; + SetVisible(false); } } height += margin; - BDGUIUtils.RepositionWindow(ref window); - BDGUIUtils.UseMouseEventInRect(window); + GUIUtils.RepositionWindow(ref window); + GUIUtils.UseMouseEventInRect(window); } protected virtual void OnGUI() @@ -124,7 +131,7 @@ protected virtual void OnGUI() && Event.current.type == EventType.MouseDown && !window.Contains(Event.current.mousePosition)) { - open = false; + SetVisible(false); } if (open && BDArmorySetup.GAME_UI_ENABLED) @@ -135,11 +142,11 @@ protected virtual void OnGUI() width, scrollable ? Screen.height / 2 + buttonHeight + buttonGap + 2 * margin : height); window = GUI.Window(10591029, clientRect, TeamSelectorWindow, "", BDArmorySetup.BDGuiSkin.window); - Utils.UpdateGUIRect(window, guiCheckIndex); + GUIUtils.UpdateGUIRect(window, guiCheckIndex); } else { - Utils.UpdateGUIRect(new Rect(), guiCheckIndex); + GUIUtils.UpdateGUIRect(new Rect(), guiCheckIndex); } } } @@ -163,11 +170,10 @@ private void OnDestroy() private IEnumerator WaitForBdaSettings() { - while (BDArmorySetup.Instance == null) - yield return null; + yield return new WaitUntil(() => BDArmorySetup.Instance is not null); ready = true; - guiCheckIndex = Utils.RegisterGUIRect(new Rect()); + if (guiCheckIndex < 0) guiCheckIndex = GUIUtils.RegisterGUIRect(new Rect()); } } } diff --git a/BDArmory/UI/KrakensbaneDebug.cs b/BDArmory/UI/KrakensbaneDebug.cs index 5b5fa880d..4cfd1fb03 100644 --- a/BDArmory/UI/KrakensbaneDebug.cs +++ b/BDArmory/UI/KrakensbaneDebug.cs @@ -3,8 +3,9 @@ // This will only be live in debug builds using System; using UnityEngine; -using BDArmory.Core; -using BDArmory.Misc; + +using BDArmory.Settings; +using BDArmory.Utils; namespace BDArmory.UI { @@ -15,19 +16,21 @@ public class KrakensbaneDebug : MonoBehaviour void FixedUpdate() { - if (!FloatingOrigin.Offset.IsZero()) + if (BDKrakensbane.IsActive) lastShift = Time.time; } void OnGUI() { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_TELEMETRY) { - var frameVelocity = Krakensbane.GetFrameVelocityV3f(); + var frameVelocity = BDKrakensbane.FrameVelocityV3f; //var rFrameVelocity = FlightGlobals.currentMainBody.getRFrmVel(Vector3d.zero); //var rFrameRotation = rFrameVelocity - FlightGlobals.currentMainBody.getRFrmVel(VectorUtils.GetUpDirection(Vector3.zero)); - GUI.Label(new Rect(10, 60, 400, 400), + GUI.Label(new Rect(10, 150, 400, 400), $"Frame velocity: {frameVelocity.magnitude} ({frameVelocity}){Environment.NewLine}" + + $"FO offset: {(Vector3)BDKrakensbane.FloatingOriginOffset:G3}{Environment.NewLine}" + + $"N-Kb offset: {(Vector3)BDKrakensbane.FloatingOriginOffsetNonKrakensbane:G3}{Environment.NewLine}" + $"Last offset {Time.time - lastShift}s ago{Environment.NewLine}" + $"Local vessel speed: {FlightGlobals.ActiveVessel.rb_velocity.magnitude}, ({FlightGlobals.ActiveVessel.rb_velocity}){Environment.NewLine}" //+ $"Reference frame speed: {rFrameVelocity}{Environment.NewLine}" diff --git a/BDArmory/UI/LoadedVesselSwitcher.cs b/BDArmory/UI/LoadedVesselSwitcher.cs index 8ab69c665..0857e72c8 100644 --- a/BDArmory/UI/LoadedVesselSwitcher.cs +++ b/BDArmory/UI/LoadedVesselSwitcher.cs @@ -1,17 +1,18 @@ -using KSP.Localization; using System.Collections.Generic; using System.Collections; using System.IO; using System.Linq; +using System.Text; using System; using UnityEngine; using BDArmory.Competition; -using BDArmory.Competition.VesselSpawning; -using BDArmory.Core.Extension; -using BDArmory.Core; -using BDArmory.Misc; -using BDArmory.Modules; +using BDArmory.Control; +using BDArmory.Extensions; +using BDArmory.Settings; +using BDArmory.Utils; +using BDArmory.VesselSpawning; +using BDArmory.Weapons.Missiles; namespace BDArmory.UI { @@ -21,7 +22,7 @@ public class LoadedVesselSwitcher : MonoBehaviour private readonly float _buttonGap = 1; private readonly float _buttonHeight = 20; - private int _guiCheckIndex; + private static int _guiCheckIndex = -1; public static LoadedVesselSwitcher Instance; private readonly float _margin = 5; @@ -32,14 +33,15 @@ public class LoadedVesselSwitcher : MonoBehaviour private readonly float _titleHeight = 30; private double lastCameraSwitch = 0; private double lastCameraCheck = 0; + double minCameraCheckInterval = 0.25; private Vessel lastActiveVessel = null; private bool currentVesselDied = false; private double currentVesselDiedAt = 0; - private float updateTimer = 0; //gui params private float _windowHeight; //auto adjusting - + private string camMode = "A"; + private int currentMode = 1; private SortedList> weaponManagers = new SortedList>(); private Dictionary cameraScores = new Dictionary(); @@ -54,6 +56,16 @@ public SortedList> WeaponManagers } } + private Dictionary> _vessels = new Dictionary>(); + public Dictionary> Vessels + { + get + { + if (!upToDateWMs) UpdateList(); + return _vessels; + } + } + // booleans to track state of buttons affecting everyone private bool _teamsAssigned = false; private bool _autoPilotEnabled = false; @@ -136,61 +148,55 @@ private void OnDestroy() private IEnumerator WaitForBdaSettings() { - while (BDArmorySetup.Instance == null) - yield return null; + yield return new WaitUntil(() => BDArmorySetup.Instance is not null); _ready = true; BDArmorySetup.Instance.hasVesselSwitcher = true; - _guiCheckIndex = Utils.RegisterGUIRect(new Rect()); + if (_guiCheckIndex < 0) _guiCheckIndex = GUIUtils.RegisterGUIRect(new Rect()); + SetVisible(BDArmorySetup.showVesselSwitcherGUI); } private void MissileFireOnToggleTeam(MissileFire wm, BDTeam team) { - if (_showGui) - UpdateList(); + UpdateList(); } private void VesselEventUpdate(Vessel v) { - if (_showGui) - UpdateList(); + UpdateList(); } private void Update() { - if (_ready) + if (!_ready) return; + if (BDArmorySetup.showVesselSwitcherGUI != _showGui) { - upToDateWMs = false; - if (BDArmorySetup.Instance.showVesselSwitcherGUI != _showGui) - { - updateTimer -= Time.fixedDeltaTime; - _showGui = BDArmorySetup.Instance.showVesselSwitcherGUI; - if (_showGui && updateTimer < 0) - { - UpdateList(); - updateTimer = 0.5f; //next update in half a sec only - } - } + _showGui = BDArmorySetup.showVesselSwitcherGUI; + UpdateList(); + } - if (_showGui) - { - Hotkeys(); - } + if (_showGui) + { + Hotkeys(); + } - // check for camera changes - if (_autoCameraSwitch) - { - UpdateCamera(); - } + // check for camera changes + if (_autoCameraSwitch) + { + UpdateCamera(); } } void FixedUpdate() { + if (!_ready) return; + + if (WeaponManagers.SelectMany(tm => tm.Value).Any(wm => wm == null)) upToDateWMs = false; + if (vesselTraceEnabled) { - if (!FloatingOrigin.Offset.IsZero() || !Krakensbane.GetFrameVelocity().IsZero()) - floatingOriginCorrection += FloatingOrigin.OffsetNonKrakensbane; + if (BDKrakensbane.IsActive) + floatingOriginCorrection += BDKrakensbane.FloatingOriginOffsetNonKrakensbane; var survivingVessels = weaponManagers.SelectMany(tm => tm.Value).Where(wm => wm != null).Select(wm => wm.vessel).ToList(); foreach (var vessel in survivingVessels) { @@ -214,7 +220,8 @@ public void UpdateList() { weaponManagers.Clear(); - if (FlightGlobals.Vessels == null) return; + try { if (FlightGlobals.Vessels == null) return; } // Sometimes this gets called multiple times when exiting KSP due to something repeatedly calling DestroyImmediate on a vessel! + catch { return; } using (var v = FlightGlobals.Vessels.GetEnumerator()) while (v.MoveNext()) { @@ -229,6 +236,18 @@ public void UpdateList() weaponManagers.Add(wms.Team.Name, new List { wms }); } } + + _vessels.Clear(); + using (var team = weaponManagers.Keys.GetEnumerator()) + while (team.MoveNext()) + { + _vessels.Add(team.Current, new List()); + using (var wm = weaponManagers[team.Current].GetEnumerator()) + while (wm.MoveNext()) + { + _vessels[team.Current].Add(wm.Current.vessel); + } + } upToDateWMs = true; } @@ -275,42 +294,60 @@ private void ToggleAutopilots() private void OnGUI() { - if (_ready) + if (!(_ready && HighLogic.LoadedSceneIsFlight)) + return; + if (_showGui && (BDArmorySetup.GAME_UI_ENABLED || BDArmorySettings.VESSEL_SWITCHER_PERSIST_UI)) { - if (_showGui && BDArmorySetup.GAME_UI_ENABLED || (BDArmorySettings.VESSEL_SWITCHER_PERSIST_UI && !BDArmorySetup.GAME_UI_ENABLED)) - { - string windowTitle = Localizer.Format("#LOC_BDArmory_BDAVesselSwitcher_Title"); - if (BDArmorySettings.GRAVITY_HACKS) - windowTitle = windowTitle + " (" + BDACompetitionMode.gravityMultiplier.ToString("0.0") + "G)"; - - SetNewHeight(_windowHeight); - // this Rect initialization ensures any save issues with height or width of the window are resolved - BDArmorySetup.WindowRectVesselSwitcher = new Rect(BDArmorySetup.WindowRectVesselSwitcher.x, BDArmorySetup.WindowRectVesselSwitcher.y, BDArmorySettings.VESSEL_SWITCHER_WINDOW_WIDTH, _windowHeight); - BDArmorySetup.WindowRectVesselSwitcher = GUI.Window(10293444, BDArmorySetup.WindowRectVesselSwitcher, WindowVesselSwitcher, windowTitle, BDArmorySetup.BDGuiSkin.window); //"BDA Vessel Switcher" - Utils.UpdateGUIRect(BDArmorySetup.WindowRectVesselSwitcher, _guiCheckIndex); - } - else - { - Utils.UpdateGUIRect(new Rect(), _guiCheckIndex); - } + string windowTitle = StringUtils.Localize("#LOC_BDArmory_BDAVesselSwitcher_Title"); + if (BDArmorySettings.GRAVITY_HACKS) + windowTitle = windowTitle + " (" + BDACompetitionMode.gravityMultiplier.ToString("0.0") + "G)"; + + SetNewHeight(_windowHeight); + // this Rect initialization ensures any save issues with height or width of the window are resolved + BDArmorySetup.WindowRectVesselSwitcher = new Rect(BDArmorySetup.WindowRectVesselSwitcher.x, BDArmorySetup.WindowRectVesselSwitcher.y, BDArmorySettings.VESSEL_SWITCHER_WINDOW_WIDTH, _windowHeight); + BDArmorySetup.SetGUIOpacity(); + BDArmorySetup.WindowRectVesselSwitcher = GUI.Window(10293444, BDArmorySetup.WindowRectVesselSwitcher, WindowVesselSwitcher, windowTitle, BDArmorySetup.BDGuiSkin.window); //"BDA Vessel Switcher" + GUIUtils.UpdateGUIRect(BDArmorySetup.WindowRectVesselSwitcher, _guiCheckIndex); + BDArmorySetup.SetGUIOpacity(false); + } + else + { + GUIUtils.UpdateGUIRect(new Rect(), _guiCheckIndex); } } + public void SetVisible(bool visible) + { + BDArmorySetup.showVesselSwitcherGUI = visible; + GUIUtils.SetGUIRectVisible(_guiCheckIndex, visible); + } + private void SetNewHeight(float windowHeight) { var previousWindowHeight = BDArmorySetup.WindowRectVesselSwitcher.height; BDArmorySetup.WindowRectVesselSwitcher.height = windowHeight; if (BDArmorySettings.STRICT_WINDOW_BOUNDARIES && windowHeight < previousWindowHeight && Mathf.RoundToInt(BDArmorySetup.WindowRectVesselSwitcher.y + previousWindowHeight) == Screen.height) // Window shrunk while being at edge of screen. BDArmorySetup.WindowRectVesselSwitcher.y = Screen.height - BDArmorySetup.WindowRectVesselSwitcher.height; - BDGUIUtils.RepositionWindow(ref BDArmorySetup.WindowRectVesselSwitcher); + GUIUtils.RepositionWindow(ref BDArmorySetup.WindowRectVesselSwitcher); + } + + public void ResetDeadVessels() => deadVesselStrings.Clear(); // Reset the dead vessel strings so that they get recalculated. + Dictionary deadVesselStrings = new Dictionary(); // Cache dead vessel strings (they could potentially change during a competition, so we'll reset them at the end of competitions). + StringBuilder deadVesselString = new StringBuilder(); + + float WaypointRank(string player) + { + if (!BDACompetitionMode.Instance.Scores.ScoreData.ContainsKey(player)) return 0f; + var score = BDACompetitionMode.Instance.Scores.ScoreData[player]; + return (float)score.waypointsReached.Count - 0.001f * score.totalWPTime; // Rank in the VS based primarily on #waypoints passed and secondly on time. } private void WindowVesselSwitcher(int id) { - int numButtons = 10; + int numButtons = 11; int numButtonsOnLeft = 5; GUI.DragWindow(new Rect(numButtonsOnLeft * _buttonHeight + _margin, 0f, BDArmorySettings.VESSEL_SWITCHER_WINDOW_WIDTH - numButtons * _buttonHeight - 3f * _margin, _titleHeight)); - + GUI.Label(new Rect(BDArmorySettings.VESSEL_SWITCHER_WINDOW_WIDTH - (numButtons - numButtonsOnLeft) * _buttonHeight - _margin - 70f, 4f, 70f, _titleHeight - 4f), BDArmorySetup.Version); if (GUI.Button(new Rect(0f * _buttonHeight + _margin, 4f, _buttonHeight, _buttonHeight), "><", BDArmorySetup.BDGuiSkin.button)) // Don't get so small that the buttons get hidden. { BDArmorySettings.VESSEL_SWITCHER_WINDOW_WIDTH -= 50f; @@ -354,11 +391,35 @@ private void WindowVesselSwitcher(int id) } } - if (GUI.Button(new Rect(BDArmorySettings.VESSEL_SWITCHER_WINDOW_WIDTH - 5 * _buttonHeight - _margin, 4, _buttonHeight, _buttonHeight), "A", _autoCameraSwitch ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button)) + if (GUI.Button(new Rect(BDArmorySettings.VESSEL_SWITCHER_WINDOW_WIDTH - 5 * _buttonHeight - _margin, 4, _buttonHeight, _buttonHeight), camMode, _autoCameraSwitch ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button)) { - // set/disable automatic camera switching - _autoCameraSwitch = !_autoCameraSwitch; - Debug.Log("[BDArmory.LoadedVesselSwitcher]: Setting AutoCameraSwitch"); + if (Event.current.button == 1) //right click + { + switch (++currentMode) + { + case 2: + camMode = "S"; //Score-based camera tracking + break; + case 3: + camMode = "D"; //Distance-based camera tracking + break; + default: + camMode = "A"; //Algorithm-based camera tracking + currentMode = 1; + break; + } + } + else if (Event.current.button == 2) //mouse 3 + { + camMode = "A"; + currentMode = 1; + } + else + { + // set/disable automatic camera switching + _autoCameraSwitch = !_autoCameraSwitch; + Debug.Log("[BDArmory.LoadedVesselSwitcher]: Setting AutoCameraSwitch"); + } } if (GUI.Button(new Rect(BDArmorySettings.VESSEL_SWITCHER_WINDOW_WIDTH - 4 * _buttonHeight - _margin, 4, _buttonHeight, _buttonHeight), "G", _guardModeEnabled ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button)) @@ -388,9 +449,9 @@ private void WindowVesselSwitcher(int id) } } - if (GUI.Button(new Rect(BDArmorySettings.VESSEL_SWITCHER_WINDOW_WIDTH - _buttonHeight - _margin, 4, _buttonHeight, _buttonHeight), "X", BDArmorySetup.BDGuiSkin.button)) + if (GUI.Button(new Rect(BDArmorySettings.VESSEL_SWITCHER_WINDOW_WIDTH - _buttonHeight - _margin, 4, _buttonHeight, _buttonHeight), " X", BDArmorySetup.CloseButtonStyle)) { - BDArmorySetup.Instance.showVesselSwitcherGUI = false; + SetVisible(false); return; } @@ -417,7 +478,25 @@ private void WindowVesselSwitcher(int id) } catch (Exception e) { - Debug.LogError("[BDArmory.LoadedVesselSwitcher]: AddVesselSwitcherWindowEntry threw an exception trying to add " + weaponManagerPair.Item2.vessel.vesselName + " on team " + weaponManagerPair.Item1 + " to the list: " + e.Message); + Debug.LogError($"[BDArmory.LoadedVesselSwitcher]: AddVesselSwitcherWindowEntry threw an exception trying to add {weaponManagerPair.Item2.vessel.vesselName} on team {weaponManagerPair.Item1} to the list: {e.Message}"); + } + height += _buttonHeight + _buttonGap; + } + } + else if (BDArmorySettings.WAYPOINTS_MODE) + { + var orderedWMs = weaponManagers.SelectMany(tm => tm.Value, (tm, weaponManager) => new Tuple(tm.Key, weaponManager)).Where(t => t.Item2 != null && t.Item2.vessel != null).ToList(); // Use a local copy. + orderedWMs.Sort((mf1, mf2) => WaypointRank(mf2.Item2.vessel.vesselName).CompareTo(WaypointRank(mf1.Item2.vessel.vesselName))); + foreach (var weaponManagerPair in orderedWMs) + { + if (weaponManagerPair.Item1 == null) continue; + try + { + AddVesselSwitcherWindowEntry(weaponManagerPair.Item2, weaponManagerPair.Item1, height, vesselButtonWidth); + } + catch (Exception e) + { + Debug.LogError($"[BDArmory.LoadedVesselSwitcher]: AddVesselSwitcherWindowEntry threw an exception trying to add {weaponManagerPair.Item2.vessel.vesselName} on team {weaponManagerPair.Item1} to the list: {e.Message}"); } height += _buttonHeight + _buttonGap; } @@ -429,13 +508,13 @@ private void WindowVesselSwitcher(int id) { foreach (var teamManager in orderedTeamManagers) teamManager.Item2.Sort((wm1, wm2) => ((ContinuousSpawning.Instance.continuousSpawningScores.ContainsKey(wm2.vessel.vesselName) ? ContinuousSpawning.Instance.continuousSpawningScores[wm2.vessel.vesselName].cumulativeHits : 0) + (BDACompetitionMode.Instance.Scores.Players.Contains(wm2.vessel.vesselName) ? BDACompetitionMode.Instance.Scores.ScoreData[wm2.vessel.vesselName].hits : 0)).CompareTo((ContinuousSpawning.Instance.continuousSpawningScores.ContainsKey(wm1.vessel.vesselName) ? ContinuousSpawning.Instance.continuousSpawningScores[wm1.vessel.vesselName].cumulativeHits : 0) + (BDACompetitionMode.Instance.Scores.Players.Contains(wm1.vessel.vesselName) ? BDACompetitionMode.Instance.Scores.ScoreData[wm1.vessel.vesselName].hits : 0))); // Sort within each team by cumulative hits. - orderedTeamManagers.Sort((tm1, tm2) => (tm2.Item2.Sum(wm => (ContinuousSpawning.Instance.continuousSpawningScores.ContainsKey(wm.vessel.vesselName) ? ContinuousSpawning.Instance.continuousSpawningScores[wm.vessel.vesselName].cumulativeHits : 0) + (BDACompetitionMode.Instance.Scores.Players.Contains(wm.vessel.vesselName) ? BDACompetitionMode.Instance.Scores.ScoreData[wm.vessel.GetName()].hits : 0)).CompareTo(tm1.Item2.Sum(wm => (ContinuousSpawning.Instance.continuousSpawningScores.ContainsKey(wm.vessel.vesselName) ? ContinuousSpawning.Instance.continuousSpawningScores[wm.vessel.vesselName].cumulativeHits : 0) + (BDACompetitionMode.Instance.Scores.Players.Contains(wm.vessel.vesselName) ? BDACompetitionMode.Instance.Scores.ScoreData[wm.vessel.GetName()].hits : 0))))); // Sort teams by total cumulative hits. + orderedTeamManagers.Sort((tm1, tm2) => (tm2.Item2.Sum(wm => (ContinuousSpawning.Instance.continuousSpawningScores.ContainsKey(wm.vessel.vesselName) ? ContinuousSpawning.Instance.continuousSpawningScores[wm.vessel.vesselName].cumulativeHits : 0) + (BDACompetitionMode.Instance.Scores.Players.Contains(wm.vessel.vesselName) ? BDACompetitionMode.Instance.Scores.ScoreData[wm.vessel.vesselName].hits : 0)).CompareTo(tm1.Item2.Sum(wm => (ContinuousSpawning.Instance.continuousSpawningScores.ContainsKey(wm.vessel.vesselName) ? ContinuousSpawning.Instance.continuousSpawningScores[wm.vessel.vesselName].cumulativeHits : 0) + (BDACompetitionMode.Instance.Scores.Players.Contains(wm.vessel.vesselName) ? BDACompetitionMode.Instance.Scores.ScoreData[wm.vessel.vesselName].hits : 0))))); // Sort teams by total cumulative hits. } else { foreach (var teamManager in orderedTeamManagers) teamManager.Item2.Sort((wm1, wm2) => (BDACompetitionMode.Instance.Scores.Players.Contains(wm2.vessel.vesselName) ? BDACompetitionMode.Instance.Scores.ScoreData[wm2.vessel.vesselName].hits : 0).CompareTo(BDACompetitionMode.Instance.Scores.Players.Contains(wm1.vessel.vesselName) ? BDACompetitionMode.Instance.Scores.ScoreData[wm1.vessel.vesselName].hits : 0)); // Sort within each team by hits. - orderedTeamManagers.Sort((tm1, tm2) => (tm2.Item2.Sum(wm => BDACompetitionMode.Instance.Scores.Players.Contains(wm.vessel.vesselName) ? BDACompetitionMode.Instance.Scores.ScoreData[wm.vessel.GetName()].hits : 0).CompareTo(tm1.Item2.Sum(wm => BDACompetitionMode.Instance.Scores.Players.Contains(wm.vessel.vesselName) ? BDACompetitionMode.Instance.Scores.ScoreData[wm.vessel.GetName()].hits : 0)))); // Sort teams by total hits. + orderedTeamManagers.Sort((tm1, tm2) => (tm2.Item2.Sum(wm => BDACompetitionMode.Instance.Scores.Players.Contains(wm.vessel.vesselName) ? BDACompetitionMode.Instance.Scores.ScoreData[wm.vessel.vesselName].hits : 0).CompareTo(tm1.Item2.Sum(wm => BDACompetitionMode.Instance.Scores.Players.Contains(wm.vessel.vesselName) ? BDACompetitionMode.Instance.Scores.ScoreData[wm.vessel.vesselName].hits : 0)))); // Sort teams by total hits. } foreach (var teamManager in orderedTeamManagers) { @@ -450,7 +529,7 @@ private void WindowVesselSwitcher(int id) { BDTISetup.TILabel.normal.textColor = BDTISetup.Instance.ColorAssignments[teamManager.Item1]; } - GUI.Label(new Rect(_margin, height, BDArmorySettings.VESSEL_SWITCHER_WINDOW_WIDTH - 2 * _margin, _buttonHeight), $"{teamManager.Item1}:" + (weaponManager.Team.Neutral ? (weaponManager.Team.Name != "Neutral" ? "(Neutral)" : "") : ""), BDTISetup.TILabel); + GUI.Label(new Rect(_margin, height, BDArmorySettings.VESSEL_SWITCHER_WINDOW_WIDTH - 2 * _margin, _buttonHeight), $"{teamManager.Item1}:{(weaponManager.Team.Neutral ? (weaponManager.Team.Name != "Neutral" ? "(Neutral)" : "") : "")}", BDTISetup.TILabel); teamNameShowing = true; height += _buttonHeight + _buttonGap; @@ -461,7 +540,7 @@ private void WindowVesselSwitcher(int id) } catch (Exception e) { - Debug.LogError("[BDArmory.LoadedVesselSwitcher]: AddVesselSwitcherWindowEntry threw an exception trying to add " + weaponManager.vessel.vesselName + " on team " + teamManager.Item1 + " to the list: " + e.Message); + Debug.LogError($"[BDArmory.LoadedVesselSwitcher]: AddVesselSwitcherWindowEntry threw an exception trying to add {weaponManager.vessel.vesselName} on team {teamManager.Item1} to the list: {e.Message}"); } height += _buttonHeight + _buttonGap; } @@ -482,7 +561,7 @@ private void WindowVesselSwitcher(int id) { BDTISetup.TILabel.normal.textColor = BDTISetup.Instance.ColorAssignments[teamManagers.Key]; } - GUI.Label(new Rect(_margin, height, BDArmorySettings.VESSEL_SWITCHER_WINDOW_WIDTH - 2 * _margin, _buttonHeight), $"{teamManagers.Key}:" + (weaponManager.team != "Neutral" ? (weaponManager.Team.Neutral ? "(Neutral)" : "") : ""), BDTISetup.TILabel); + GUI.Label(new Rect(_margin, height, BDArmorySettings.VESSEL_SWITCHER_WINDOW_WIDTH - 2 * _margin, _buttonHeight), $"{teamManagers.Key}:{(weaponManager.team != "Neutral" ? (weaponManager.Team.Neutral ? "(Neutral)" : "") : "")}", BDTISetup.TILabel); teamNameShowing = true; height += _buttonHeight + _buttonGap; } @@ -492,7 +571,7 @@ private void WindowVesselSwitcher(int id) } catch (Exception e) { - Debug.LogError("[BDArmory.LoadedVesselSwitcher]: AddVesselSwitcherWindowEntry threw an exception trying to add " + weaponManager.vessel.vesselName + " on team " + teamManagers.Key + " to the list: " + e.Message); + Debug.LogError($"[BDArmory.LoadedVesselSwitcher]: AddVesselSwitcherWindowEntry threw an exception trying to add {weaponManager.vessel.vesselName} on team {teamManagers.Key} to the list: {e.Message}"); } height += _buttonHeight + _buttonGap; } @@ -504,86 +583,98 @@ private void WindowVesselSwitcher(int id) { foreach (var player in BDACompetitionMode.Instance.Scores.deathOrder) { - string statusString = ""; - // DEAD :: vesselName([, ][, ])[ KILLED|RAMMED BY ], where is the number of hits made is the number of parts destroyed. - statusString += "DEAD " + BDACompetitionMode.Instance.Scores.ScoreData[player].deathOrder + ":" + BDACompetitionMode.Instance.Scores.ScoreData[player].deathTime.ToString("0.0") + " : " + player + " (" + BDACompetitionMode.Instance.Scores.ScoreData[player].hits.ToString(); - if (BDACompetitionMode.Instance.Scores.ScoreData[player].totalDamagedPartsDueToRockets > 0) - statusString += ", " + BDACompetitionMode.Instance.Scores.ScoreData[player].totalDamagedPartsDueToRockets; - if (BDACompetitionMode.Instance.Scores.ScoreData[player].totalDamagedPartsDueToMissiles > 0) - statusString += ", " + BDACompetitionMode.Instance.Scores.ScoreData[player].totalDamagedPartsDueToMissiles; - if (BDACompetitionMode.Instance.Scores.ScoreData[player].totalDamagedPartsDueToRamming > 0) - statusString += ", " + BDACompetitionMode.Instance.Scores.ScoreData[player].totalDamagedPartsDueToRamming; - if (ContinuousSpawning.Instance.vesselsSpawningContinuously && BDACompetitionMode.Instance.Scores.ScoreData[player].tagTotalTime > 0) - statusString += ", " + BDACompetitionMode.Instance.Scores.ScoreData[player].tagTotalTime.ToString("0.0"); - else if (BDACompetitionMode.Instance.Scores.ScoreData[player].tagScore > 0) - statusString += ", " + BDACompetitionMode.Instance.Scores.ScoreData[player].tagScore.ToString("0.0"); - switch (BDACompetitionMode.Instance.Scores.ScoreData[player].lastDamageWasFrom) + if (BDACompetitionMode.Instance.hasPinata && player == BDArmorySettings.PINATA_NAME) continue; // Ignore the piñata. + if (!deadVesselStrings.ContainsKey(player)) { - case DamageFrom.Guns: - statusString += ") KILLED BY " + BDACompetitionMode.Instance.Scores.ScoreData[player].lastPersonWhoDamagedMe; - break; - case DamageFrom.Rockets: - statusString += ") FRAGGED BY " + BDACompetitionMode.Instance.Scores.ScoreData[player].lastPersonWhoDamagedMe; - break; - case DamageFrom.Missiles: - statusString += ") EXPLODED BY " + BDACompetitionMode.Instance.Scores.ScoreData[player].lastPersonWhoDamagedMe; - break; - case DamageFrom.Ramming: - statusString += ") RAMMED BY " + BDACompetitionMode.Instance.Scores.ScoreData[player].lastPersonWhoDamagedMe; - break; - case DamageFrom.Incompetence: - statusString += ") CRASHED AND BURNED!"; - break; - case DamageFrom.None: - statusString += ") " + BDACompetitionMode.Instance.Scores.ScoreData[player].gmKillReason; - break; - default: // Note: All the cases ought to be covered above. - statusString += ")"; - break; - } - switch (BDACompetitionMode.Instance.Scores.ScoreData[player].aliveState) - { - case AliveState.CleanKill: - statusString += " (Clean-Kill!)"; - break; - case AliveState.HeadShot: - statusString += " (Head-Shot!)"; - break; - case AliveState.KillSteal: - statusString += " (Kill-Steal!)"; - break; - case AliveState.AssistedKill: - statusString += ", et al."; - break; - case AliveState.Dead: - break; + deadVesselString.Clear(); + // DEAD :: vesselName([, ][, ])[ KILLED|RAMMED BY ], where is the number of hits made is the number of parts destroyed. + deadVesselString.Append($"DEAD {BDACompetitionMode.Instance.Scores.ScoreData[player].deathOrder}:{BDACompetitionMode.Instance.Scores.ScoreData[player].deathTime:0.0} : {player} ({BDACompetitionMode.Instance.Scores.ScoreData[player].hits} hits"); + if (BDACompetitionMode.Instance.Scores.ScoreData[player].totalDamagedPartsDueToRockets > 0) + deadVesselString.Append($", {BDACompetitionMode.Instance.Scores.ScoreData[player].totalDamagedPartsDueToRockets} rkt"); + if (BDACompetitionMode.Instance.Scores.ScoreData[player].totalDamagedPartsDueToMissiles > 0) + deadVesselString.Append($", {BDACompetitionMode.Instance.Scores.ScoreData[player].totalDamagedPartsDueToMissiles} mis"); + if (BDACompetitionMode.Instance.Scores.ScoreData[player].totalDamagedPartsDueToRamming > 0) + deadVesselString.Append($", {BDACompetitionMode.Instance.Scores.ScoreData[player].totalDamagedPartsDueToRamming} ram"); + if (ContinuousSpawning.Instance.vesselsSpawningContinuously && BDACompetitionMode.Instance.Scores.ScoreData[player].tagTotalTime > 0) + deadVesselString.Append($", {BDACompetitionMode.Instance.Scores.ScoreData[player].tagTotalTime:0.0} tag"); + else if (BDACompetitionMode.Instance.Scores.ScoreData[player].tagScore > 0) + deadVesselString.Append($", {BDACompetitionMode.Instance.Scores.ScoreData[player].tagScore:0.0} tag"); + switch (BDACompetitionMode.Instance.Scores.ScoreData[player].lastDamageWasFrom) + { + case DamageFrom.Guns: + deadVesselString.Append($") KILLED BY {BDACompetitionMode.Instance.Scores.ScoreData[player].lastPersonWhoDamagedMe}"); + break; + case DamageFrom.Rockets: + deadVesselString.Append($") FRAGGED BY {BDACompetitionMode.Instance.Scores.ScoreData[player].lastPersonWhoDamagedMe}"); + break; + case DamageFrom.Missiles: + deadVesselString.Append($") EXPLODED BY {BDACompetitionMode.Instance.Scores.ScoreData[player].lastPersonWhoDamagedMe}"); + break; + case DamageFrom.Ramming: + deadVesselString.Append($") RAMMED BY {BDACompetitionMode.Instance.Scores.ScoreData[player].lastPersonWhoDamagedMe}"); + break; + case DamageFrom.Asteroids: + deadVesselString.Append($") FLEW INTO AN ASTEROID!"); + break; + case DamageFrom.Incompetence: + deadVesselString.Append(") CRASHED AND BURNED!"); + break; + case DamageFrom.None: + deadVesselString.Append($") {BDACompetitionMode.Instance.Scores.ScoreData[player].gmKillReason}"); + break; + default: // Note: All the cases ought to be covered above. + deadVesselString.Append(")"); + break; + } + switch (BDACompetitionMode.Instance.Scores.ScoreData[player].aliveState) + { + case AliveState.CleanKill: + deadVesselString.Append(" (Clean-Kill!)"); + break; + case AliveState.HeadShot: + deadVesselString.Append(" (Head-Shot!)"); + break; + case AliveState.KillSteal: + deadVesselString.Append(" (Kill-Steal!)"); + break; + case AliveState.AssistedKill: + deadVesselString.Append(", et al."); + break; + case AliveState.Dead: + break; + } + deadVesselStrings.Add(player, deadVesselString.ToString()); } - GUI.Label(new Rect(_margin, height, vesselButtonWidth, _buttonHeight), statusString, BDArmorySetup.BDGuiSkin.label); + GUI.Label(new Rect(_margin, height, BDArmorySettings.VESSEL_SWITCHER_WINDOW_WIDTH - 2 * _margin, _buttonHeight), deadVesselStrings[player], BDArmorySetup.BDGuiSkin.label); // Use the full width since we're not showing buttons here. height += _buttonHeight + _buttonGap; } } // Piñata killers. - if (!BDACompetitionMode.Instance.pinataAlive) + if (BDACompetitionMode.Instance.hasPinata && !BDACompetitionMode.Instance.pinataAlive) { - string postString = ""; - foreach (var player in BDACompetitionMode.Instance.Scores.Players) + if (!deadVesselStrings.ContainsKey(BDArmorySettings.PINATA_NAME)) { - if (BDACompetitionMode.Instance.Scores.ScoreData[player].PinataHits > 0) + deadVesselString.Clear(); + deadVesselString.Append("Pinata Killers: "); + foreach (var player in BDACompetitionMode.Instance.Scores.Players) { - postString += " " + player; + if (BDACompetitionMode.Instance.Scores.ScoreData[player].PinataHits > 0) //not reporting any players? + { + deadVesselString.Append($" {player};"); + //BDACompetitionMode.Instance.Scores.ScoreData[BDArmorySettings.PINATA_NAME].lastPersonWhoDamagedMe + } } + deadVesselStrings.Add(BDArmorySettings.PINATA_NAME, deadVesselString.ToString()); } - if (postString != "") - { - GUI.Label(new Rect(_margin, height, vesselButtonWidth, _buttonHeight), "Pinata Killers: " + postString, BDArmorySetup.BDGuiSkin.label); - height += _buttonHeight + _buttonGap; - } + GUI.Label(new Rect(_margin, height, vesselButtonWidth, _buttonHeight), deadVesselStrings[BDArmorySettings.PINATA_NAME], BDArmorySetup.BDGuiSkin.label); + height += _buttonHeight + _buttonGap; } height += _margin; - _windowHeight = height; + _windowHeight = Mathf.Max(height, _titleHeight + _buttonHeight); } + StringBuilder VSEntryString = new StringBuilder(); void AddVesselSwitcherWindowEntry(MissileFire wm, string team, float height, float vesselButtonWidth) { float _offset = 0; @@ -599,9 +690,16 @@ void AddVesselSwitcherWindowEntry(MissileFire wm, string team, float height, flo Rect buttonRect = new Rect(_margin + _offset, height, vesselButtonWidth, _buttonHeight); GUIStyle vButtonStyle = team == "IT" ? (wm.vessel.isActiveVessel ? ItVesselSelected : ItVessel) : wm.vessel.isActiveVessel ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button; - string vesselName = wm.vessel.GetName(); + VSEntryString.Clear(); + string vesselName = wm.vessel.vesselName; + VSEntryString.Append(vesselName); + if (BDArmorySettings.HALL_OF_SHAME_LIST.Contains(vesselName)) + { + // vesselName += " (HoS)"; + VSEntryString.Append(" (HoS)"); + } + VSEntryString.Append(UpdateVesselStatus(wm, vButtonStyle)); // status ScoringData scoreData = null; - string status = UpdateVesselStatus(wm, vButtonStyle); int currentScore = 0; int currentRocketScore = 0; int currentRamScore = 0; @@ -637,33 +735,35 @@ void AddVesselSwitcherWindowEntry(MissileFire wm, string team, float height, flo currentTagTime += ContinuousSpawning.Instance.continuousSpawningScores[wm.vessel.vesselName].cumulativeTagTime; } - // current target - string targetName = ""; - Vessel targetVessel = wm.vessel; - bool incomingThreat = false; - if (wm.incomingThreatVessel != null) + // string postStatus = " (" + currentScore.ToString(); + // if (currentRocketScore > 0) postStatus += ", " + currentRocketScore.ToString(); + // if (currentRamScore > 0) postStatus += ", " + currentRamScore.ToString(); + // if (currentMissileScore > 0) postStatus += ", " + currentMissileScore.ToString(); + // if (BDArmorySettings.TAG_MODE) + // postStatus += ", " + (ContinuousSpawning.Instance.vesselsSpawningContinuously ? currentTagTime.ToString("0.0") : currentTagScore.ToString("0.0")); + // postStatus += ")"; + if (BDArmorySettings.WAYPOINTS_MODE) { - incomingThreat = true; - targetName = "<<<" + wm.incomingThreatVessel.GetName(); - targetVessel = wm.incomingThreatVessel; + if (scoreData != null) // This probably won't work if running waypoints in continuous spawning mode, but that probably doesn't work anyway! + { + VSEntryString.Append($" ({scoreData.waypointsReached.Count:0}, {scoreData.totalWPTime:0.0}s, {scoreData.totalWPDeviation:0.00}m), "); + } } - else if (wm.currentTarget) + else { - targetName = ">>>" + wm.currentTarget.Vessel.GetName(); - targetVessel = wm.currentTarget.Vessel; + VSEntryString.Append($" ({currentScore}"); + if (currentRocketScore > 0) VSEntryString.Append($", {currentRocketScore} rkt"); + if (currentMissileScore > 0) VSEntryString.Append($", {currentMissileScore} mis"); + if (currentRamScore > 0) VSEntryString.Append($", {currentRamScore} ram"); + if (BDArmorySettings.TAG_MODE) + VSEntryString.Append($", {(ContinuousSpawning.Instance.vesselsSpawningContinuously ? currentTagTime : currentTagScore):0.0} tag"); + VSEntryString.Append(")"); } - string postStatus = " (" + currentScore.ToString(); - if (currentRocketScore > 0) postStatus += ", " + currentRocketScore.ToString(); - if (currentRamScore > 0) postStatus += ", " + currentRamScore.ToString(); - if (currentMissileScore > 0) postStatus += ", " + currentMissileScore.ToString(); - if (BDArmorySettings.TAG_MODE) - postStatus += ", " + (ContinuousSpawning.Instance.vesselsSpawningContinuously ? currentTagTime.ToString("0.0") : currentTagScore.ToString("0.0")); - postStatus += ")"; - if (wm.AI != null && wm.AI.currentStatus != null) { - postStatus += " " + wm.AI.currentStatus; + // postStatus += " " + wm.AI.currentStatus; + VSEntryString.Append($" {wm.AI.currentStatus}"); } float targetDistance = 5000; if (wm.currentTarget != null) @@ -680,12 +780,30 @@ void AddVesselSwitcherWindowEntry(MissileFire wm, string team, float height, flo if (BDACompetitionMode.Instance.KillTimer.ContainsKey(vesselName)) { - postStatus += " x" + BDACompetitionMode.Instance.KillTimer[vesselName].ToString() + "x"; + // postStatus += " x" + BDACompetitionMode.Instance.KillTimer[vesselName].ToString() + "x"; + VSEntryString.Append($" x{BDACompetitionMode.Instance.KillTimer[vesselName]}x"); + } + + // current target + string targetName = ""; + Vessel targetVessel = wm.vessel; + bool incomingThreat = false; + if (wm.incomingThreatVessel != null) + { + incomingThreat = true; + targetName = $"<<<{wm.incomingThreatVessel.vesselName}"; + targetVessel = wm.incomingThreatVessel; + } + else if (wm.currentTarget) + { + targetName = $">>>{wm.currentTarget.Vessel.vesselName}"; + targetVessel = wm.currentTarget.Vessel; } if (targetName != "") { - postStatus += " " + targetName; + // postStatus += " " + targetName; + VSEntryString.Append($" {targetName}"); } /*if (cameraScores.ContainsKey(vesselName)) @@ -694,11 +812,7 @@ void AddVesselSwitcherWindowEntry(MissileFire wm, string team, float height, flo postStatus += " [" + sc.ToString() + "]"; } */ - if (BDArmorySettings.HALL_OF_SHAME_LIST.Contains(vesselName)) - { - vesselName += " (HoS)"; - } - if (GUI.Button(buttonRect, vesselName + status + postStatus, vButtonStyle)) + if (GUI.Button(buttonRect, VSEntryString.ToString(), vButtonStyle)) ForceSwitchVessel(wm.vessel); // selects current target @@ -817,32 +931,33 @@ void AddVesselSwitcherWindowEntry(MissileFire wm, string team, float height, flo scoreData.lastPersonWhoDamagedMe = "BIG RED BUTTON"; // only do this if it's not already damaged } BDACompetitionMode.Instance.Scores.RegisterDeath(vesselName, GMKillReason.BigRedButton); // Indicate that it was us who killed it. - BDACompetitionMode.Instance.competitionStatus.Add(vesselName + " was killed by the BIG RED BUTTON."); + BDACompetitionMode.Instance.competitionStatus.Add($"{vesselName} {(BDArmorySettings.HALL_OF_SHAME_LIST.Contains(vesselName) ? " (HoS)" : "")} was killed by the BIG RED BUTTON."); } - Utils.ForceDeadVessel(wm.vessel); + VesselUtils.ForceDeadVessel(wm.vessel); } } } + StringBuilder vesselStatusString = new StringBuilder(); private string UpdateVesselStatus(MissileFire wm, GUIStyle vButtonStyle) { - string status = ""; + vesselStatusString.Clear(); if (wm.vessel.LandedOrSplashed) { - status = " "; + vesselStatusString.Append(" "); if (wm.vessel.Landed) - status += Localizer.Format("#LOC_BDArmory_VesselStatus_Landed");//"(Landed)" + vesselStatusString.Append(StringUtils.Localize("#LOC_BDArmory_VesselStatus_Landed"));//"(Landed)" else if (wm.vessel.IsUnderwater()) - status += Localizer.Format("#LOC_BDArmory_VesselStatus_Underwater"); // "(Underwater)" + vesselStatusString.Append(StringUtils.Localize("#LOC_BDArmory_VesselStatus_Underwater")); // "(Underwater)" else - status += Localizer.Format("#LOC_BDArmory_VesselStatus_Splashed");//"(Splashed)" + vesselStatusString.Append(StringUtils.Localize("#LOC_BDArmory_VesselStatus_Splashed"));//"(Splashed)" vButtonStyle.fontStyle = FontStyle.Italic; } else { vButtonStyle.fontStyle = FontStyle.Normal; } - return status; + return vesselStatusString.ToString(); } private void SwitchToNextVessel() @@ -883,7 +998,7 @@ public void MassTeamSwitch(bool separateTeams = false, bool originalTeams = fals { if (SpawnUtils.originalTeams.ContainsKey(weaponManager.vessel.vesselName)) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.LoadedVesselSwitcher]: assigning " + weaponManager.vessel.GetDisplayName() + " to team " + SpawnUtils.originalTeams[weaponManager.vessel.vesselName]); + if (BDArmorySettings.DEBUG_AI) Debug.Log("[BDArmory.LoadedVesselSwitcher]: assigning " + weaponManager.vessel.GetDisplayName() + " to team " + SpawnUtils.originalTeams[weaponManager.vessel.vesselName]); weaponManager.SetTeam(BDTeam.Get(SpawnUtils.originalTeams[weaponManager.vessel.vesselName])); } } @@ -934,7 +1049,7 @@ public void MassTeamSwitch(bool separateTeams = false, bool originalTeams = fals // switch everyone to their own teams foreach (var weaponManager in weaponManagers.SelectMany(tm => tm.Value).Where(wm => wm != null).ToList()) // Get a copy in case activating stages causes the weaponManager list to change. { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.LoadedVesselSwitcher]: assigning " + weaponManager.vessel.GetDisplayName() + " to team " + T.ToString()); + if (BDArmorySettings.DEBUG_AI) Debug.Log("[BDArmory.LoadedVesselSwitcher]: assigning " + weaponManager.vessel.GetDisplayName() + " to team " + T.ToString()); weaponManager.SetTeam(BDTeam.Get(T.ToString())); weaponManager.Team.Neutral = false; if (separateTeams) T++; @@ -967,14 +1082,22 @@ void CurrentVesselWillDestroy(Vessel v) { if (_autoCameraSwitch && lastActiveVessel == v) { - currentVesselDied = true; - currentVesselDiedAt = Planetarium.GetUniversalTime(); + if (v.IsMissile()) + { + currentVesselDied = true; + currentVesselDiedAt = Time.time - (BDArmorySettings.DEATH_CAMERA_SWITCH_INHIBIT_PERIOD == 0 ? BDArmorySettings.CAMERA_SWITCH_FREQUENCY / 2f : BDArmorySettings.DEATH_CAMERA_SWITCH_INHIBIT_PERIOD) + minCameraCheckInterval; + } + else + { + currentVesselDied = true; + currentVesselDiedAt = Time.time; + } } } private void UpdateCamera() { - var now = Planetarium.GetUniversalTime(); + var now = Time.time; double timeSinceLastCheck = now - lastCameraCheck; if (currentVesselDied) { @@ -984,10 +1107,12 @@ private void UpdateCamera() { currentVesselDied = false; lastCameraSwitch = 0; + lastActiveVessel = null; + timeSinceLastCheck = minCameraCheckInterval + 1f; } } - if (timeSinceLastCheck > 0.25) + if (timeSinceLastCheck > minCameraCheckInterval) { lastCameraCheck = now; @@ -1000,28 +1125,108 @@ private void UpdateCamera() lastCameraSwitch = now; } } - lastActiveVessel = FlightGlobals.ActiveVessel; double timeSinceChange = now - lastCameraSwitch; - float bestScore = 10000000; + float bestScore = currentMode > 1 ? 0 : 10000000; Vessel bestVessel = null; + var activeVessel = FlightGlobals.ActiveVessel; + if (activeVessel != null && activeVessel.loaded && !activeVessel.packed && activeVessel.IsMissile()) + { + var mb = VesselModuleRegistry.GetMissileBase(activeVessel); + if (mb != null && !mb.HasMissed && Vector3.Dot((mb.TargetPosition - mb.vessel.transform.position).normalized, mb.vessel.transform.up) < 0.5f) return; // Don't switch away from an active missile until it misses or is off-target. + } bool foundActiveVessel = false; - // redo the math - using (var v = FlightGlobals.Vessels.GetEnumerator()) + Vector3 centroid = Vector3.zero; + if (currentMode == 3) //distance-based + { + int count = 1; + + foreach (var v in WeaponManagers.SelectMany(tm => tm.Value).Where(wm => wm != null && wm.vessel != null).Select(wm => wm.vessel)) + { + if (v.vesselType != VesselType.Debris) + { + centroid += v.CoM; + ++count; + } + } + centroid /= (float)count; + } + if (BDArmorySettings.CAMERA_SWITCH_INCLUDE_MISSILES) // Prioritise active missiles. // FIXME Not sure this bit is actually doing much. + { + foreach (MissileBase missile in BDATargetManager.FiredMissiles) + { + if (missile == null || missile.HasMissed) continue; // Ignore missed missiles. + var targetDirection = missile.TargetPosition - missile.transform.position; + var targetDistance = targetDirection.magnitude; + if (Vector3.Dot(targetDirection, missile.vessel.up) < 0.5f * targetDistance) continue; // Ignore off-target missiles. + float missileScore = targetDistance < 1.1e3f ? 1e-3f : (targetDistance - 1e3f) * (targetDistance - 1e3f) * 1e-10f; // Prioritise missiles that are within 1km from their targets. + if (missileScore < bestScore) + { + bestScore = missileScore; + bestVessel = missile.vessel; + } + } + } + using (var wm = WeaponManagers.SelectMany(tm => tm.Value).Where(wm => wm != null && wm.vessel != null).ToList().GetEnumerator()) + // redo the math // check all the planes - while (v.MoveNext()) + while (wm.MoveNext()) { - if (v.Current == null || !v.Current.loaded || v.Current.packed) continue; - if (VesselModuleRegistry.ignoredVesselTypes.Contains(v.Current.vesselType)) continue; - if ((v.Current.GetCrewCapacity()) > 0 && (v.Current.GetCrewCount() == 0)) continue; //They're dead, Jim - using (var wms = VesselModuleRegistry.GetModules(v.Current).GetEnumerator()) - while (wms.MoveNext()) - if (wms.Current != null && wms.Current.vessel != null) + //if ((v.Current.GetCrewCapacity()) > 0 && (v.Current.GetCrewCount() == 0)) continue; //They're dead, Jim //really should be a isControllable tage, else this will never look at ProbeCore ships + if (wm.Current == null || wm.Current.vessel == null) continue; + if (!wm.Current.vessel.IsControllable) continue; + float vesselScore = 1000; + switch (currentMode) + { + case 2: //score-based + { + ScoringData scoreData = null; + int score = 0; + if (BDACompetitionMode.Instance.Scores.ScoreData.ContainsKey(wm.Current.vessel.vesselName)) + { + scoreData = BDACompetitionMode.Instance.Scores.ScoreData[wm.Current.vessel.vesselName]; + score = scoreData.hits; //expand to something closer to the score parser score? + } + if (ContinuousSpawning.Instance.vesselsSpawningContinuously) + { + if (ContinuousSpawning.Instance.continuousSpawningScores.ContainsKey(wm.Current.vessel.vesselName)) + { + score += ContinuousSpawning.Instance.continuousSpawningScores[wm.Current.vessel.vesselName].cumulativeHits; + } + } + if (wm.Current.vessel.isActiveVessel) + { + foundActiveVessel = true; + } + if (score > 0) vesselScore = score; + if (vesselScore > bestScore) + { + bestVessel = wm.Current.vessel; + bestScore = vesselScore; + } + cameraScores[wm.Current.vessel.vesselName] = vesselScore; + break; + } + case 3: //distance based - look for most distant vessel from centroid; use with CameraTools centroid option + { + vesselScore = (centroid - wm.Current.vessel.CoM).magnitude; + if (vesselScore > bestScore) + { + bestVessel = wm.Current.vessel; + bestScore = vesselScore; + } + if (wm.Current.vessel.isActiveVessel) + { + foundActiveVessel = true; + } + cameraScores[wm.Current.vessel.vesselName] = vesselScore; + break; + } + default: { - float vesselScore = 1000; float targetDistance = 5000 + (float)(rng.NextDouble() * 100.0); float crashTime = 30; - string vesselName = v.Current.GetName(); + string vesselName = wm.Current.vessel.vesselName; // avoid lingering on dying things bool recentlyDamaged = false; bool recentlyLanded = false; @@ -1030,7 +1235,7 @@ private void UpdateCamera() if (BDACompetitionMode.Instance.Scores.Players.Contains(vesselName)) { - var currentParts = v.Current.parts.Count; + var currentParts = wm.Current.vessel.parts.Count; var vdat = BDACompetitionMode.Instance.Scores.ScoreData[vesselName]; if (now - vdat.lastLostPartTime < 5d) // Lost parts within the last 5s. { @@ -1049,33 +1254,38 @@ private void UpdateCamera() vesselScore = Math.Abs(vesselScore); float HP = 0; float WreckFactor = 0; - var AI = VesselModuleRegistry.GetBDModulePilotAI(v.Current, true); - + var AI = VesselModuleRegistry.GetBDModulePilotAI(wm.Current.vessel, true); + // If we're running a waypoints competition, only focus on vessels still running waypoints. - if (BDACompetitionMode.Instance.competitionType == CompetitionType.WAYPOINTS && AI != null && AI.currentCommand != Control.PilotCommands.Waypoints) continue; + if (BDACompetitionMode.Instance.competitionType == CompetitionType.WAYPOINTS) + { + if (AI == null || !AI.IsRunningWaypoints) continue; + vesselScore *= 2f - Mathf.Clamp01((float)wm.Current.vessel.speed / AI.maxSpeed); // For waypoints races, craft going near their max speed are more interesting. + vesselScore *= Mathf.Max(0.5f, 1f - 15.8f / BDAMath.Sqrt(AI.waypointRange)); // Favour craft the are approaching a gate (capped at 1km). + } - HP = (wms.Current.currentHP / wms.Current.totalHP) * 100; - if (HP < 100) + HP = wm.Current.currentHP / wm.Current.totalHP; + if (HP < 1) { - WreckFactor += (100 - HP) / 100; //the less plane remaining, the greater the chance it's a wreck + WreckFactor += 1f - HP * HP; //the less plane remaining, the greater the chance it's a wreck } - if (v.Current.verticalSpeed < -30) //falling out of the sky? Could be an intact plane diving to default alt, could be a cockpit + if (wm.Current.vessel.verticalSpeed < -30) //falling out of the sky? Could be an intact plane diving to default alt, could be a cockpit { WreckFactor += 0.5f; - if (AI == null || v.Current.radarAltitude < AI.defaultAltitude) //craft is uncontrollably diving, not returning from high alt to cruising alt + if (AI == null || wm.Current.vessel.radarAltitude < AI.defaultAltitude) //craft is uncontrollably diving, not returning from high alt to cruising alt { WreckFactor += 0.5f; } } - if (VesselModuleRegistry.GetModuleCount(v.Current) > 0) + if (VesselModuleRegistry.GetModuleCount(wm.Current.vessel) > 0) { int engineOut = 0; - foreach (var engine in VesselModuleRegistry.GetModules(v.Current)) + foreach (var engine in VesselModuleRegistry.GetModules(wm.Current.vessel)) { if (engine == null || engine.flameout || engine.finalThrust <= 0) engineOut++; } - WreckFactor += (engineOut / VesselModuleRegistry.GetModuleCount(v.Current)) / 2; + WreckFactor += (engineOut / VesselModuleRegistry.GetModuleCount(wm.Current.vessel)) / 2; } else { @@ -1086,22 +1296,22 @@ private void UpdateCamera() WreckFactor *= 2; vesselScore *= WreckFactor; //disincentivise switching to wrecks } - if (!recentlyLanded && v.Current.verticalSpeed < -15) // Vessels gently floating to the ground aren't interesting + if (!recentlyLanded && wm.Current.vessel.verticalSpeed < -15) // Vessels gently floating to the ground aren't interesting { - crashTime = (float)(-Math.Abs(v.Current.radarAltitude) / v.Current.verticalSpeed); + crashTime = (float)(-Math.Abs(wm.Current.vessel.radarAltitude) / wm.Current.vessel.verticalSpeed); } - if (crashTime < 30) + if (crashTime < 30 && HP > 0.5f) { vesselScore *= crashTime / 30; } - if (wms.Current.currentTarget != null) + if (wm.Current.currentTarget != null) { - targetDistance = Vector3.Distance(wms.Current.vessel.GetWorldPos3D(), wms.Current.currentTarget.position); - if (!wms.Current.HasWeaponsAndAmmo()) // no remaining weapons + targetDistance = Vector3.Distance(wm.Current.vessel.GetWorldPos3D(), wm.Current.currentTarget.position); + if (!wm.Current.HasWeaponsAndAmmo()) // no remaining weapons { if (!BDArmorySettings.DISABLE_RAMMING && AI != null && AI.allowRamming) //ramming's fun to watch { - vesselScore *= (0.031623f * Mathf.Sqrt(targetDistance) / 2); + vesselScore *= (0.031623f * BDAMath.Sqrt(targetDistance) / 2); } else { @@ -1110,78 +1320,77 @@ private void UpdateCamera() } //else got weapons and engaging } - vesselScore *= 0.031623f * Mathf.Sqrt(targetDistance); // Equal to 1 at 1000m - if (wms.Current.currentGun != null) - { - if (wms.Current.currentGun.recentlyFiring) - { - // shooting at things is more interesting - vesselScore *= 0.25f; - } - } - if (wms.Current.guardFiringMissile) - { - // firing a missile at things is more interesting - vesselScore *= 0.2f; - } + vesselScore *= 0.031623f * BDAMath.Sqrt(targetDistance); // Equal to 1 at 1000m + if (wm.Current.recentlyFiring) // Firing guns or missiles at stuff is more interesting. (Uses 1/2 the camera switch frequency on all guns.) + vesselScore *= 0.25f; + if (wm.Current.guardFiringMissile) // Firing missiles is a bit more interesting than firing guns. + vesselScore *= 0.8f; + if (wm.Current.currentGun != null && wm.Current.currentGun.recentlyFiring && wm.Current.vessel == FlightGlobals.ActiveVessel) // 1s timer on current gun. + vesselScore *= 0.1f; // Actively firing guns on the current vessel are even more interesting, try not to switch away at the last second! // scoring for automagic camera check should not be in here - if (wms.Current.underAttack || wms.Current.underFire) + if (wm.Current.underAttack || wm.Current.underFire) { vesselScore *= 0.5f; - var distance = Vector3.Distance(wms.Current.vessel.GetWorldPos3D(), wms.Current.incomingThreatPosition); - vesselScore *= 0.031623f * Mathf.Sqrt(distance); // Equal to 1 at 1000m, we don't want to overly disadvantage craft that are super far away, but could be firing missiles or doing other interesting things - //we're very interested when threat and target are the same - if (wms.Current.incomingThreatVessel != null && wms.Current.currentTarget != null) + var distance = Vector3.Distance(wm.Current.vessel.GetWorldPos3D(), wm.Current.incomingThreatPosition); + vesselScore *= 0.031623f * BDAMath.Sqrt(distance); // Equal to 1 at 1000m, we don't want to overly disadvantage craft that are super far away, but could be firing missiles or doing other interesting things + //we're very interested when threat and target are the same + if (wm.Current.incomingThreatVessel != null && wm.Current.currentTarget != null) { - if (wms.Current.incomingThreatVessel.GetName() == wms.Current.currentTarget.Vessel.GetName()) + if (wm.Current.incomingThreatVessel.vesselName == wm.Current.currentTarget.Vessel.vesselName) { vesselScore *= 0.25f; } } - } - if (wms.Current.incomingMissileVessel != null) + if (wm.Current.incomingMissileVessel != null) { - float timeToImpact = wms.Current.incomingMissileTime; + float timeToImpact = wm.Current.incomingMissileTime; vesselScore *= Mathf.Clamp(0.0005f * timeToImpact * timeToImpact, 0, 1); // Missiles about to hit are interesting, scale score with time to impact - if (wms.Current.isFlaring || wms.Current.isChaffing) + if (wm.Current.isFlaring || wm.Current.isChaffing) vesselScore *= 0.8f; } if (recentlyDamaged) { vesselScore *= 0.3f; // because taking hits is very interesting; } - if (!recentlyLanded && wms.Current.vessel.LandedOrSplashed) + if (wm.Current.vessel.LandedOrSplashed) { - if (v.Current.srfSpeed > 2) //margin for physics jitter + if (wm.Current.vessel.srfSpeed > 2) //margin for physics jitter { - vesselScore *= Mathf.Min(((80 / (float)v.Current.srfSpeed) / 2), 4); //srf Ai driven stuff thats still mobile + vesselScore *= Mathf.Min(((80 / (float)wm.Current.vessel.srfSpeed) / 2), 4); //srf Ai driven stuff thats still mobile } else - vesselScore *= 4; // not interesting. + { + if (recentlyLanded) + vesselScore *= 2; // less interesting. + else + vesselScore *= 4; // not interesting. + } } // if we're the active vessel add a penalty over time to force it to switch away eventually - if (wms.Current.vessel.isActiveVessel) + if (wm.Current.vessel.isActiveVessel) { vesselScore = (float)(vesselScore * timeSinceChange / 8.0); foundActiveVessel = true; } - if ((BDArmorySettings.TAG_MODE) && (wms.Current.Team.Name == "IT")) + if ((BDArmorySettings.TAG_MODE) && (wm.Current.Team.Name == "IT")) { vesselScore = 0f; // Keep camera focused on "IT" vessel during tag } - // if the score is better then update this if (vesselScore < bestScore) { - bestVessel = wms.Current.vessel; + bestVessel = wm.Current.vessel; bestScore = vesselScore; } - cameraScores[wms.Current.vessel.GetName()] = vesselScore; + cameraScores[wm.Current.vessel.vesselName] = vesselScore; + break; } + } } + lastActiveVessel = FlightGlobals.ActiveVessel; if (!foundActiveVessel) { var score = 100 * timeSinceChange; @@ -1190,16 +1399,27 @@ private void UpdateCamera() bestVessel = null; // stop switching } } - if (timeSinceChange > BDArmorySettings.CAMERA_SWITCH_FREQUENCY) + if (timeSinceChange > BDArmorySettings.CAMERA_SWITCH_FREQUENCY * timeScaleSqrt) { if (bestVessel != null && bestVessel.loaded && !bestVessel.packed && !(bestVessel.isActiveVessel)) // if a vessel dies it'll use a default score for a few seconds { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.LoadedVesselSwitcher]: Switching vessel to " + bestVessel.GetDisplayName()); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log("[BDArmory.LoadedVesselSwitcher]: Switching vessel to " + bestVessel.GetDisplayName()); ForceSwitchVessel(bestVessel); } } } } + float _timeScaleSqrt = 1f; + float timeScaleSqrt // For scaling the camera switch frequency with the sqrt of the time scale. + { + get + { + if (!BDArmorySettings.TIME_OVERRIDE || Time.timeScale <= 1) return 1f; + if (Mathf.Abs(Time.timeScale - _timeScaleSqrt * _timeScaleSqrt) > 1e-3f) + _timeScaleSqrt = BDAMath.Sqrt(Time.timeScale); + return _timeScaleSqrt; + } + } public void EnableAutoVesselSwitching(bool enable) { @@ -1211,14 +1431,27 @@ public void ForceSwitchVessel(Vessel v) { if (v == null || !v.loaded) return; - lastCameraSwitch = Planetarium.GetUniversalTime(); + lastCameraSwitch = Time.time; + lastActiveVessel = v; + var camHeading = FlightCamera.CamHdg; + var camPitch = FlightCamera.CamPitch; FlightGlobals.ForceSetActiveVessel(v); FlightInputHandler.ResumeVesselCtrlState(v); + FlightCamera.CamHdg = camHeading; + FlightCamera.CamPitch = camPitch; + } + + public IEnumerator SwitchToVesselWhenPossible(Vessel vessel, float distance = 0) + { + var wait = new WaitForFixedUpdate(); + while (vessel != null && (!vessel.loaded || vessel.packed)) yield return wait; + while (vessel != null && vessel.loaded && vessel != FlightGlobals.ActiveVessel) { ForceSwitchVessel(vessel); yield return wait; } + if (distance > 0) FlightCamera.fetch.SetDistance(distance); } - public void TriggerSwitchVessel(float delay) + public void TriggerSwitchVessel(float delay = 0) { - lastCameraSwitch = delay > 0 ? Planetarium.GetUniversalTime() - (BDArmorySettings.CAMERA_SWITCH_FREQUENCY - delay) : 0f; + lastCameraSwitch = delay > 0 ? Time.time - (BDArmorySettings.CAMERA_SWITCH_FREQUENCY * timeScaleSqrt - delay) : 0f; lastCameraCheck = 0f; UpdateCamera(); } diff --git a/BDArmory/UI/RemoteOrchestrationWindow.cs b/BDArmory/UI/RemoteOrchestrationWindow.cs index b1ced5195..ea91ef1fd 100644 --- a/BDArmory/UI/RemoteOrchestrationWindow.cs +++ b/BDArmory/UI/RemoteOrchestrationWindow.cs @@ -1,9 +1,10 @@ using System.Collections; using UnityEngine; -using BDArmory.Core; -using BDArmory.Misc; using KSP.Localization; + using BDArmory.Competition.RemoteOrchestration; +using BDArmory.Settings; +using BDArmory.Utils; namespace BDArmory.UI { @@ -14,7 +15,7 @@ public class RemoteOrchestrationWindow : MonoBehaviour private BDAScoreService service; private BDAScoreClient client; - private int _guiCheckIndex; + private static int _guiCheckIndex = -1; private readonly float _titleHeight = 30; private readonly float _margin = 5; @@ -59,14 +60,16 @@ private void OnGUI() BDArmorySettings.REMOTE_ORCHESTRATION_WINDOW_WIDTH, _windowHeight ); + BDArmorySetup.SetGUIOpacity(); BDArmorySetup.WindowRectRemoteOrchestration = GUI.Window( 80085, BDArmorySetup.WindowRectRemoteOrchestration, WindowRemoteOrchestration, - Localizer.Format("#LOC_BDArmory_BDARemoteOrchestration_Title"),//"BDA Remote Orchestration" + StringUtils.Localize("#LOC_BDArmory_BDARemoteOrchestration_Title"),//"BDA Remote Orchestration" BDArmorySetup.BDGuiSkin.window ); - Utils.UpdateGUIRect(BDArmorySetup.WindowRectRemoteOrchestration, _guiCheckIndex); + BDArmorySetup.SetGUIOpacity(false); + GUIUtils.UpdateGUIRect(BDArmorySetup.WindowRectRemoteOrchestration, _guiCheckIndex); } private void SetNewHeight(float windowHeight) @@ -87,19 +90,22 @@ public void ShowWindow() { if (!ready) StartCoroutine(WaitForSetup()); - showWindow = true; + SetVisible(true); + } + + void SetVisible(bool visible) + { + showWindow = visible; + GUIUtils.SetGUIRectVisible(_guiCheckIndex, visible); } private IEnumerator WaitForSetup() { - while (BDArmorySetup.Instance == null || BDAScoreService.Instance == null || BDAScoreService.Instance.client == null) - { - yield return null; - } + yield return new WaitWhile(() => BDArmorySetup.Instance == null || BDAScoreService.Instance == null || BDAScoreService.Instance.client == null); service = BDAScoreService.Instance; UpdateClientStatus(); ready = true; - _guiCheckIndex = Utils.RegisterGUIRect(new Rect()); + if (_guiCheckIndex < 0) _guiCheckIndex = GUIUtils.RegisterGUIRect(new Rect()); } private void WindowRemoteOrchestration(int id) @@ -107,7 +113,7 @@ private void WindowRemoteOrchestration(int id) GUI.DragWindow(new Rect(0, 0, BDArmorySettings.REMOTE_ORCHESTRATION_WINDOW_WIDTH - _titleHeight / 2 - 2, _titleHeight)); if (GUI.Button(new Rect(BDArmorySettings.REMOTE_ORCHESTRATION_WINDOW_WIDTH - _titleHeight / 2 - 2, 2, _titleHeight / 2, _titleHeight / 2), "X", BDArmorySetup.BDGuiSkin.button)) { - showWindow = false; + SetVisible(false); } float offset = _titleHeight + _margin; @@ -172,7 +178,7 @@ private void WindowRemoteOrchestration(int id) _windowHeight = offset; - BDGUIUtils.RepositionWindow(ref BDArmorySetup.WindowRectRemoteOrchestration); // Prevent it from going off the screen edges. + GUIUtils.RepositionWindow(ref BDArmorySetup.WindowRectRemoteOrchestration); // Prevent it from going off the screen edges. } } } diff --git a/BDArmory/UI/VesselSpawnerWindow.cs b/BDArmory/UI/VesselSpawnerWindow.cs index c4441ae88..e4f194360 100644 --- a/BDArmory/UI/VesselSpawnerWindow.cs +++ b/BDArmory/UI/VesselSpawnerWindow.cs @@ -7,12 +7,12 @@ using UnityEngine; using BDArmory.Competition.OrchestrationStrategies; -using BDArmory.Competition.SpawnStrategies; -using BDArmory.Competition.VesselSpawning; using BDArmory.Competition; -using BDArmory.Core.Utils; -using BDArmory.Core; -using BDArmory.Misc; +using BDArmory.GameModes.Waypoints; +using BDArmory.Settings; +using BDArmory.Utils; +using BDArmory.VesselSpawning.SpawnStrategies; +using BDArmory.VesselSpawning; namespace BDArmory.UI { @@ -21,7 +21,7 @@ public class VesselSpawnerWindow : MonoBehaviour { #region Fields public static VesselSpawnerWindow Instance; - private int _guiCheckIndex; + private static int _guiCheckIndex = -1; private static readonly float _buttonSize = 20; private static readonly float _margin = 5; private static readonly float _lineHeight = _buttonSize; @@ -83,9 +83,24 @@ Rect SRightButtonRect(float line) return new Rect(_windowWidth / 2 + _margin / 4, line * _lineHeight, (_windowWidth - 2 * _margin) / 2 - _margin / 4, _lineHeight); } - Rect SQuarterRect(float line, int pos) + Rect SThirdRect(float line, int pos, int span = 1, float indent = 0) { - return new Rect(_margin + (pos % 4) * (_windowWidth - 2f * _margin) / 4f, (line + (int)(pos / 4)) * _lineHeight, (_windowWidth - 2.5f * _margin) / 4f, _lineHeight); + return new Rect(_margin + pos * (_windowWidth - 2f * _margin) / 3f + indent, line * _lineHeight, span * (_windowWidth - 2f * _margin) / 3f - indent, _lineHeight); + } + + Rect SQuarterRect(float line, int pos, int span = 1, float indent = 0) + { + return new Rect(_margin + (pos % 4) * (_windowWidth - 2f * _margin) / 4f + indent, (line + (int)(pos / 4)) * _lineHeight, span * (_windowWidth - 2f * _margin) / 4f - indent, _lineHeight); + } + + Rect SEighthRect(float line, int pos, int span = 1, float indent = 0) + { + return new Rect(_margin + (pos % 8) * (_windowWidth - 2f * _margin) / 8f + indent, (line + (int)(pos / 8)) * _lineHeight, span * (_windowWidth - 2f * _margin) / 8f - indent, _lineHeight); + } + + Rect ShortLabel(float line, float width) + { + return new Rect(_margin, line * _lineHeight, width, _lineHeight); } List SRight2Rects(float line) @@ -110,6 +125,7 @@ List SRight3Rects(float line) } GUIStyle leftLabel; + GUIStyle listStyle; #endregion private string txtName = string.Empty; private void Awake() @@ -127,6 +143,8 @@ private void Start() leftLabel = new GUIStyle(); leftLabel.alignment = TextAnchor.UpperLeft; leftLabel.normal.textColor = Color.white; + listStyle = new GUIStyle(BDArmorySetup.BDGuiSkin.button); + listStyle.fixedHeight = 18; //make list contents slightly smaller // Spawn fields spawnFields = new Dictionary { @@ -153,12 +171,13 @@ private void Start() private IEnumerator WaitForBdaSettings() { - while (BDArmorySetup.Instance == null) - yield return null; + yield return new WaitUntil(() => BDArmorySetup.Instance is not null); BDArmorySetup.Instance.hasVesselSpawner = true; - _guiCheckIndex = Utils.RegisterGUIRect(new Rect()); + if (_guiCheckIndex < 0) _guiCheckIndex = GUIUtils.RegisterGUIRect(new Rect()); + if (_observerGUICheckIndex < 0) _observerGUICheckIndex = GUIUtils.RegisterGUIRect(new Rect()); _ready = true; + SetVisible(BDArmorySetup.showVesselSpawnerGUI); } private void FillPlanetList() @@ -171,17 +190,18 @@ private void FillPlanetList() } planetText = new GUIContent(); - planetText.text = Localizer.Format("#LOC_BDArmory_Settings_Planet");//"Select Planet" + planetText.text = StringUtils.Localize("#LOC_BDArmory_Settings_Planet");//"Select Planet" } private void Update() { HotKeys(); + if (potentialObserversNeedsRefreshing) RefreshObservers(); } private void OnGUI() { - if (!(_ready && BDArmorySetup.GAME_UI_ENABLED && BDArmorySetup.Instance.showVesselSpawnerGUI)) + if (!(_ready && BDArmorySetup.GAME_UI_ENABLED && BDArmorySetup.showVesselSpawnerGUI && HighLogic.LoadedSceneIsFlight)) return; _windowWidth = BDArmorySettings.VESSEL_SPAWNER_WINDOW_WIDTH; @@ -193,14 +213,23 @@ private void OnGUI() _windowWidth, _windowHeight ); + BDArmorySetup.SetGUIOpacity(); BDArmorySetup.WindowRectVesselSpawner = GUI.Window( - GetInstanceID(), // InstanceID should be unique. FIXME All GUI.Windows should use the same method of generating unique IDs to avoid duplicates. + GUIUtility.GetControlID(FocusType.Passive), BDArmorySetup.WindowRectVesselSpawner, WindowVesselSpawner, - Localizer.Format("#LOC_BDArmory_BDAVesselSpawner_Title"),//"BDA Vessel Spawner" + StringUtils.Localize("#LOC_BDArmory_BDAVesselSpawner_Title"),//"BDA Vessel Spawner" BDArmorySetup.BDGuiSkin.window ); - Utils.UpdateGUIRect(BDArmorySetup.WindowRectVesselSpawner, _guiCheckIndex); + BDArmorySetup.SetGUIOpacity(false); + GUIUtils.UpdateGUIRect(BDArmorySetup.WindowRectVesselSpawner, _guiCheckIndex); + if (showObserverWindow) + { + if (Event.current.type == EventType.MouseDown && !observerWindowRect.Contains(Event.current.mousePosition)) + ShowObserverWindow(false); + else + observerWindowRect = GUILayout.Window(GUIUtility.GetControlID(FocusType.Passive), observerWindowRect, ObserverWindow, StringUtils.Localize("#LOC_BDArmory_ObserverSelection_Title"), BDArmorySetup.BDGuiSkin.window); + } } void HotKeys() @@ -228,22 +257,22 @@ private void SetNewHeight(float windowHeight) BDArmorySetup.WindowRectVesselSpawner.height = windowHeight; if (BDArmorySettings.STRICT_WINDOW_BOUNDARIES && windowHeight < previousWindowHeight && Mathf.RoundToInt(BDArmorySetup.WindowRectVesselSpawner.y + previousWindowHeight) == Screen.height) // Window shrunk while being at edge of screen. BDArmorySetup.WindowRectVesselSpawner.y = Screen.height - BDArmorySetup.WindowRectVesselSpawner.height; - BDGUIUtils.RepositionWindow(ref BDArmorySetup.WindowRectVesselSpawner); + GUIUtils.RepositionWindow(ref BDArmorySetup.WindowRectVesselSpawner); } private void WindowVesselSpawner(int id) { GUI.DragWindow(new Rect(0, 0, _windowWidth - _buttonSize - _margin, _buttonSize + _margin)); - if (GUI.Button(new Rect(_windowWidth - _buttonSize - (_margin - 2), _margin, _buttonSize - 2, _buttonSize - 2), "X", BDArmorySetup.BDGuiSkin.button)) + if (GUI.Button(new Rect(_windowWidth - _buttonSize - (_margin - 2), _margin, _buttonSize - 2, _buttonSize - 2), " X", BDArmorySetup.CloseButtonStyle)) { - BDArmorySetup.Instance.showVesselSpawnerGUI = false; + SetVisible(false); BDArmorySetup.SaveConfig(); } float line = 0.25f; var rects = new List(); - if (GUI.Button(SLineRect(++line), (BDArmorySettings.SHOW_SPAWN_OPTIONS ? "Hide " : "Show ") + Localizer.Format("#LOC_BDArmory_Settings_SpawnOptions"), BDArmorySettings.SHOW_SPAWN_OPTIONS ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))//Show/hide spawn options + if (GUI.Button(SLineRect(++line), $"{(BDArmorySettings.SHOW_SPAWN_OPTIONS ? StringUtils.Localize("#LOC_BDArmory_Generic_Hide") : StringUtils.Localize("#LOC_BDArmory_Generic_Show"))} {StringUtils.Localize("#LOC_BDArmory_Settings_SpawnOptions")}", BDArmorySettings.SHOW_SPAWN_OPTIONS ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))//Show/hide spawn options { BDArmorySettings.SHOW_SPAWN_OPTIONS = !BDArmorySettings.SHOW_SPAWN_OPTIONS; } @@ -252,33 +281,34 @@ private void WindowVesselSpawner(int id) if (BDArmorySettings.VESSEL_SPAWN_DISTANCE_TOGGLE) { // Absolute distance var value = BDArmorySettings.VESSEL_SPAWN_DISTANCE < 100 ? BDArmorySettings.VESSEL_SPAWN_DISTANCE / 10 : BDArmorySettings.VESSEL_SPAWN_DISTANCE < 1000 ? 9 + BDArmorySettings.VESSEL_SPAWN_DISTANCE / 100 : BDArmorySettings.VESSEL_SPAWN_DISTANCE < 10000 ? 18 + BDArmorySettings.VESSEL_SPAWN_DISTANCE / 1000 : 26 + BDArmorySettings.VESSEL_SPAWN_DISTANCE / 5000; - var displayValue = BDArmorySettings.VESSEL_SPAWN_DISTANCE < 1000 ? BDArmorySettings.VESSEL_SPAWN_DISTANCE.ToString("0") + "m" : (BDArmorySettings.VESSEL_SPAWN_DISTANCE / 1000).ToString("0") + "km"; - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_SpawnDistance")}: ({displayValue})", leftLabel);//Spawn Distance + var displayValue = BDArmorySettings.VESSEL_SPAWN_DISTANCE < 1000 ? $"{BDArmorySettings.VESSEL_SPAWN_DISTANCE:0}m" : $"{BDArmorySettings.VESSEL_SPAWN_DISTANCE / 1000:0}km"; + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_SpawnDistance")}: ({displayValue})", leftLabel);//Spawn Distance value = Mathf.Round(GUI.HorizontalSlider(SRightSliderRect(line), value, 1f, 30f)); BDArmorySettings.VESSEL_SPAWN_DISTANCE = value < 10 ? 10 * value : value < 19 ? 100 * (value - 9) : value < 28 ? 1000 * (value - 18) : 5000 * (value - 26); } else { // Distance factor - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_SpawnDistanceFactor")}: ({BDArmorySettings.VESSEL_SPAWN_DISTANCE_FACTOR})", leftLabel);//Spawn Distance Factor + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_SpawnDistanceFactor")}: ({BDArmorySettings.VESSEL_SPAWN_DISTANCE_FACTOR})", leftLabel);//Spawn Distance Factor BDArmorySettings.VESSEL_SPAWN_DISTANCE_FACTOR = Mathf.Round(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.VESSEL_SPAWN_DISTANCE_FACTOR / 10f, 1f, 10f) * 10f); } - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_SpawnEaseInSpeed")}: ({BDArmorySettings.VESSEL_SPAWN_EASE_IN_SPEED})", leftLabel);//Spawn Ease In Speed - BDArmorySettings.VESSEL_SPAWN_EASE_IN_SPEED = Mathf.Round(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.VESSEL_SPAWN_EASE_IN_SPEED, 0.1f, 1f) * 10f) / 10f; - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_SpawnConcurrentVessels")}: ({(BDArmorySettings.VESSEL_SPAWN_CONCURRENT_VESSELS > 0 ? BDArmorySettings.VESSEL_SPAWN_CONCURRENT_VESSELS.ToString() : "Inf")})", leftLabel);//Max Concurrent Vessels (CS) + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_SpawnEaseInSpeed")}: ({BDArmorySettings.VESSEL_MOVER_MIN_LOWER_SPEED})", leftLabel);//Spawn Ease-In Speed (actually the VM min lower speed) + BDArmorySettings.VESSEL_MOVER_MIN_LOWER_SPEED = BDAMath.RoundToUnit(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.VESSEL_MOVER_MIN_LOWER_SPEED, 0.1f, 1f), 0.1f); + + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_SpawnConcurrentVessels")}: ({(BDArmorySettings.VESSEL_SPAWN_CONCURRENT_VESSELS > 0 ? BDArmorySettings.VESSEL_SPAWN_CONCURRENT_VESSELS.ToString() : "Inf")})", leftLabel);//Max Concurrent Vessels (CS) BDArmorySettings.VESSEL_SPAWN_CONCURRENT_VESSELS = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.VESSEL_SPAWN_CONCURRENT_VESSELS, 0f, 20f)); - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_SpawnLivesPerVessel")}: ({(BDArmorySettings.VESSEL_SPAWN_LIVES_PER_VESSEL > 0 ? BDArmorySettings.VESSEL_SPAWN_LIVES_PER_VESSEL.ToString() : "Inf")})", leftLabel);//Respawns (CS) + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_SpawnLivesPerVessel")}: ({(BDArmorySettings.VESSEL_SPAWN_LIVES_PER_VESSEL > 0 ? BDArmorySettings.VESSEL_SPAWN_LIVES_PER_VESSEL.ToString() : "Inf")})", leftLabel);//Respawns (CS) BDArmorySettings.VESSEL_SPAWN_LIVES_PER_VESSEL = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.VESSEL_SPAWN_LIVES_PER_VESSEL, 0f, 20f)); var outOfAmmoKillTimeStr = "never"; if (BDArmorySettings.OUT_OF_AMMO_KILL_TIME > -1 && BDArmorySettings.OUT_OF_AMMO_KILL_TIME < 60) - outOfAmmoKillTimeStr = BDArmorySettings.OUT_OF_AMMO_KILL_TIME.ToString("G0") + "s"; + outOfAmmoKillTimeStr = $"{BDArmorySettings.OUT_OF_AMMO_KILL_TIME:G0}s"; else if (BDArmorySettings.OUT_OF_AMMO_KILL_TIME > 59 && BDArmorySettings.OUT_OF_AMMO_KILL_TIME < 61) outOfAmmoKillTimeStr = "1min"; else if (BDArmorySettings.OUT_OF_AMMO_KILL_TIME > 60) - outOfAmmoKillTimeStr = Mathf.RoundToInt(BDArmorySettings.OUT_OF_AMMO_KILL_TIME / 60f).ToString("G0") + "mins"; - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_OutOfAmmoKillTime")}: ({outOfAmmoKillTimeStr})", leftLabel); // Out of ammo kill timer for continuous spawning mode. + outOfAmmoKillTimeStr = $"{Mathf.RoundToInt(BDArmorySettings.OUT_OF_AMMO_KILL_TIME / 60f):G0}mins"; + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_OutOfAmmoKillTime")}: ({outOfAmmoKillTimeStr})", leftLabel); // Out of ammo kill timer for continuous spawning mode. float outOfAmmoKillTime; switch (Mathf.RoundToInt(BDArmorySettings.OUT_OF_AMMO_KILL_TIME)) { @@ -346,47 +376,61 @@ private void WindowVesselSpawner(int id) switch (BDArmorySettings.VESSEL_SPAWN_FILL_SEATS) { case 0: - fillSeats = Localizer.Format("#LOC_BDArmory_Settings_SpawnFillSeats_Minimal"); + fillSeats = StringUtils.Localize("#LOC_BDArmory_Settings_SpawnFillSeats_Minimal"); break; case 1: - fillSeats = Localizer.Format("#LOC_BDArmory_Settings_SpawnFillSeats_Default"); // Full cockpits or the first combat seat if no cockpits are found. + fillSeats = StringUtils.Localize("#LOC_BDArmory_Settings_SpawnFillSeats_Default"); // Full cockpits or the first combat seat if no cockpits are found. break; case 2: - fillSeats = Localizer.Format("#LOC_BDArmory_Settings_SpawnFillSeats_AllControlPoints"); + fillSeats = StringUtils.Localize("#LOC_BDArmory_Settings_SpawnFillSeats_AllControlPoints"); break; case 3: - fillSeats = Localizer.Format("#LOC_BDArmory_Settings_SpawnFillSeats_Cabins"); + fillSeats = StringUtils.Localize("#LOC_BDArmory_Settings_SpawnFillSeats_Cabins"); break; } - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_SpawnFillSeats")}: ({fillSeats})", leftLabel); // Fill Seats + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_SpawnFillSeats")}: ({fillSeats})", leftLabel); // Fill Seats BDArmorySettings.VESSEL_SPAWN_FILL_SEATS = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.VESSEL_SPAWN_FILL_SEATS, 0f, 3f)); string numberOfTeams; switch (BDArmorySettings.VESSEL_SPAWN_NUMBER_OF_TEAMS) { case 0: // FFA - numberOfTeams = Localizer.Format("#LOC_BDArmory_Settings_Teams_FFA"); + numberOfTeams = StringUtils.Localize("#LOC_BDArmory_Settings_Teams_FFA"); break; case 1: // Folders - numberOfTeams = Localizer.Format("#LOC_BDArmory_Settings_Teams_Folders"); + numberOfTeams = StringUtils.Localize("#LOC_BDArmory_Settings_Teams_Folders"); + break; + case 11: // Custom Template + numberOfTeams = StringUtils.Localize("#LOC_BDArmory_Settings_Teams_Custom_Template"); break; default: // Specified directly - numberOfTeams = Localizer.Format("#LOC_BDArmory_Settings_Teams_SplitEvenly") + " " + BDArmorySettings.VESSEL_SPAWN_NUMBER_OF_TEAMS.ToString("0"); + numberOfTeams = $"{StringUtils.Localize("#LOC_BDArmory_Settings_Teams_SplitEvenly")} {BDArmorySettings.VESSEL_SPAWN_NUMBER_OF_TEAMS:0}"; break; } - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_Teams")}: ({numberOfTeams})", leftLabel); // Number of teams. - BDArmorySettings.VESSEL_SPAWN_NUMBER_OF_TEAMS = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.VESSEL_SPAWN_NUMBER_OF_TEAMS, 0f, 10f)); + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_Teams")}: ({numberOfTeams})", leftLabel); // Number of teams. + BDArmorySettings.VESSEL_SPAWN_NUMBER_OF_TEAMS = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.VESSEL_SPAWN_NUMBER_OF_TEAMS, 0f, 11f)); - GUI.Label(SLeftRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_SpawnFilesLocation")} (AutoSpawn{Path.DirectorySeparatorChar}): ", leftLabel); // Craft files location + GUI.Label(SLeftRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_SpawnFilesLocation")} (AutoSpawn{Path.DirectorySeparatorChar}): ", leftLabel); // Craft files location BDArmorySettings.VESSEL_SPAWN_FILES_LOCATION = GUI.TextField(SRightRect(line), BDArmorySettings.VESSEL_SPAWN_FILES_LOCATION); - BDArmorySettings.VESSEL_SPAWN_DISTANCE_TOGGLE = GUI.Toggle(SLeftRect(++line), BDArmorySettings.VESSEL_SPAWN_DISTANCE_TOGGLE, Localizer.Format("#LOC_BDArmory_Settings_SpawnDistanceToggle")); // Toggle between distance factor and absolute distance. - BDArmorySettings.VESSEL_SPAWN_REASSIGN_TEAMS = GUI.Toggle(SRightRect(line), BDArmorySettings.VESSEL_SPAWN_REASSIGN_TEAMS, Localizer.Format("#LOC_BDArmory_Settings_SpawnReassignTeams")); // Reassign Teams - BDArmorySettings.VESSEL_SPAWN_CONTINUE_SINGLE_SPAWNING = GUI.Toggle(SLeftRect(++line), BDArmorySettings.VESSEL_SPAWN_CONTINUE_SINGLE_SPAWNING, Localizer.Format("#LOC_BDArmory_Settings_SpawnContinueSingleSpawning")); // Spawn craft again after single spawn competition finishes. - BDArmorySettings.VESSEL_SPAWN_DUMP_LOG_EVERY_SPAWN = GUI.Toggle(SRightRect(line), BDArmorySettings.VESSEL_SPAWN_DUMP_LOG_EVERY_SPAWN, Localizer.Format("#LOC_BDArmory_Settings_SpawnDumpLogsEverySpawn")); //Dump logs every spawn. - BDArmorySettings.VESSEL_SPAWN_RANDOM_ORDER = GUI.Toggle(SLeftRect(++line), BDArmorySettings.VESSEL_SPAWN_RANDOM_ORDER, Localizer.Format("#LOC_BDArmory_Settings_SpawnRandomOrder")); // Toggle between random spawn order or fixed. + BDArmorySettings.VESSEL_SPAWN_DISTANCE_TOGGLE = GUI.Toggle(SLeftRect(++line), BDArmorySettings.VESSEL_SPAWN_DISTANCE_TOGGLE, StringUtils.Localize("#LOC_BDArmory_Settings_SpawnDistanceToggle")); // Toggle between distance factor and absolute distance. + BDArmorySettings.VESSEL_SPAWN_REASSIGN_TEAMS = GUI.Toggle(SRightRect(line), BDArmorySettings.VESSEL_SPAWN_REASSIGN_TEAMS, StringUtils.Localize("#LOC_BDArmory_Settings_SpawnReassignTeams")); // Reassign Teams + BDArmorySettings.VESSEL_SPAWN_START_COMPETITION_AUTOMATICALLY = GUI.Toggle(SLeftRect(++line), BDArmorySettings.VESSEL_SPAWN_START_COMPETITION_AUTOMATICALLY, StringUtils.Localize("#LOC_BDArmory_Settings_SpawnStartCompetitionAutomatically")); // Automatically start a competition if spawning succeeds. + BDArmorySettings.VESSEL_SPAWN_RANDOM_ORDER = GUI.Toggle(SRightRect(line), BDArmorySettings.VESSEL_SPAWN_RANDOM_ORDER, StringUtils.Localize("#LOC_BDArmory_Settings_SpawnRandomOrder")); // Toggle between random spawn order or fixed. + BDArmorySettings.VESSEL_SPAWN_CONTINUE_SINGLE_SPAWNING = GUI.Toggle(SLeftRect(++line), BDArmorySettings.VESSEL_SPAWN_CONTINUE_SINGLE_SPAWNING, StringUtils.Localize("#LOC_BDArmory_Settings_SpawnContinueSingleSpawning")); // Spawn craft again after single spawn competition finishes. + BDArmorySettings.VESSEL_SPAWN_DUMP_LOG_EVERY_SPAWN = GUI.Toggle(SRightRect(line), BDArmorySettings.VESSEL_SPAWN_DUMP_LOG_EVERY_SPAWN, StringUtils.Localize("#LOC_BDArmory_Settings_SpawnDumpLogsEverySpawn")); //Dump logs every spawn. + + if (GUI.Button(SRightRect(++line), StringUtils.Localize("#LOC_BDArmory_Settings_SpawnSpawnProbeHere"), BDArmorySetup.BDGuiSkin.button)) + { + var spawnProbe = VesselSpawner.SpawnSpawnProbe(FlightCamera.fetch.Distance * FlightCamera.fetch.mainCamera.transform.forward); + if (spawnProbe != null) + { + spawnProbe.Landed = false; + StartCoroutine(LoadedVesselSwitcher.Instance.SwitchToVesselWhenPossible(spawnProbe)); + } + } - if (GUI.Button(SLeftButtonRect(++line), Localizer.Format("#LOC_BDArmory_Settings_VesselSpawnGeoCoords"), BDArmorySetup.BDGuiSkin.button)) //"Vessel Spawning Location" + if (GUI.Button(SLeftButtonRect(++line), StringUtils.Localize("#LOC_BDArmory_Settings_VesselSpawnGeoCoords"), BDArmorySetup.BDGuiSkin.button)) //"Vessel Spawning Location" { Ray ray = new Ray(FlightCamera.fetch.mainCamera.transform.position, FlightCamera.fetch.mainCamera.transform.forward); RaycastHit hit; @@ -407,27 +451,31 @@ private void WindowVesselSpawner(int id) BDArmorySettings.VESSEL_SPAWN_ALTITUDE = (float)spawnFields["alt"].currentValue; txtName = GUI.TextField(SRightButtonRect(++line), txtName); - if (GUI.Button(SLeftButtonRect(line), Localizer.Format("#LOC_BDArmory_Settings_SaveSpawnLoc"), BDArmorySetup.BDGuiSkin.button)) + if (GUI.Button(SLeftButtonRect(line), StringUtils.Localize("#LOC_BDArmory_Settings_SaveSpawnLoc"), BDArmorySetup.BDGuiSkin.button)) { string newName = string.IsNullOrEmpty(txtName.Trim()) ? "New Location" : txtName.Trim(); SpawnLocations.spawnLocations.Add(new SpawnLocation(newName, new Vector2d(spawnFields["lat"].currentValue, spawnFields["lon"].currentValue), selected_index)); VesselSpawnerField.Save(); } - if (GUI.Button(SLeftButtonRect(++line), Localizer.Format("#LOC_BDArmory_Settings_ClearDebrisNow"), BDArmorySetup.BDGuiSkin.button)) + if (GUI.Button(SThirdRect(++line, 0), StringUtils.Localize("#LOC_BDArmory_Settings_ClearDebrisNow"), BDArmorySetup.BDGuiSkin.button)) { // Clean up debris now BDACompetitionMode.Instance.RemoveDebrisNow(); } - if (GUI.Button(SRightButtonRect(line), Localizer.Format("#LOC_BDArmory_Settings_ClearBystandersNow"), BDArmorySetup.BDGuiSkin.button)) + if (GUI.Button(SThirdRect(line, 1), StringUtils.Localize("#LOC_BDArmory_Settings_ClearBystandersNow"), BDArmorySetup.BDGuiSkin.button)) { // Clean up bystanders now BDACompetitionMode.Instance.RemoveNonCompetitors(true); } + if (GUI.Button(SThirdRect(line, 2), StringUtils.Localize("#LOC_BDArmory_Settings_Observers"), BDArmorySetup.BDGuiSkin.button)) + { + ShowObserverWindow(true, Event.current.mousePosition + BDArmorySetup.WindowRectVesselSpawner.position); + } line += 0.3f; } - if (GUI.Button(SLineRect(++line), (BDArmorySettings.SHOW_SPAWN_LOCATIONS ? "Hide " : "Show ") + Localizer.Format("#LOC_BDArmory_Settings_SpawnLocations"), BDArmorySettings.SHOW_SPAWN_LOCATIONS ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))//Show/hide spawn locations + if (GUI.Button(SLineRect(++line), $"{(BDArmorySettings.SHOW_SPAWN_LOCATIONS ? StringUtils.Localize("#LOC_BDArmory_Generic_Hide") : StringUtils.Localize("#LOC_BDArmory_Generic_Show"))} {StringUtils.Localize("#LOC_BDArmory_Settings_SpawnLocations")}", BDArmorySettings.SHOW_SPAWN_LOCATIONS ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))//Show/hide spawn locations { BDArmorySettings.SHOW_SPAWN_LOCATIONS = !BDArmorySettings.SHOW_SPAWN_LOCATIONS; } @@ -438,21 +486,18 @@ private void WindowVesselSpawner(int id) if (!planetslist) { FillPlanetList(); - GUIStyle listStyle = new GUIStyle(BDArmorySetup.BDGuiSkin.button); - listStyle.fixedHeight = 18; //make list contents slightly smaller - planetBox = new BDGUIComboBox(SLeftButtonRect(line), SLeftButtonRect(line), planetText, planetGUI, _lineHeight * 6, listStyle); + planetBox = new BDGUIComboBox(SLeftButtonRect(line), SLineRect(line), planetText, planetGUI, _lineHeight * 6, listStyle, 3); planetslist = true; } planetBox.UpdateRect(SLeftButtonRect(line)); selected_index = planetBox.Show(); - if (GUI.Button(SRightButtonRect(line), Localizer.Format("#LOC_BDArmory_Settings_WarpHere"), BDArmorySetup.BDGuiSkin.button)) + if (GUI.Button(SRightButtonRect(line), StringUtils.Localize("#LOC_BDArmory_Settings_WarpHere"), BDArmorySetup.BDGuiSkin.button)) { - SpawnUtils.ShowSpawnPoint(selected_index, BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.x, BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.y, BDArmorySettings.VESSEL_SPAWN_ALTITUDE, 20); + SpawnUtils.ShowSpawnPoint(selected_index, BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.x, BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.y, BDArmorySettings.VESSEL_SPAWN_ALTITUDE); } - line += 1.3f; - if (planetBox.isOpen) + if (planetBox.IsOpen) { - line += 6.3f; + line += planetBox.Height / _lineHeight; } if (selected_index != previous_index) { @@ -470,6 +515,7 @@ private void WindowVesselSpawner(int id) previous_index = 1; } //////////////////// + ++line; int i = 0; foreach (var spawnLocation in SpawnLocations.spawnLocations) { @@ -486,7 +532,7 @@ private void WindowVesselSpawner(int id) BDArmorySettings.VESSEL_SPAWN_WORLDINDEX = spawnLocation.worldIndex; spawnFields["lat"].currentValue = BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.x; spawnFields["lon"].currentValue = BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.y; - SpawnUtils.ShowSpawnPoint(selected_index, BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.x, BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.y, BDArmorySettings.VESSEL_SPAWN_ALTITUDE, 20); + SpawnUtils.ShowSpawnPoint(selected_index, BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.x, BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.y, BDArmorySettings.VESSEL_SPAWN_ALTITUDE); break; } } @@ -495,173 +541,289 @@ private void WindowVesselSpawner(int id) line += 0.3f; } - if (BDArmorySettings.WAYPOINTS_MODE || (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 50)) // S4R10 + if (BDArmorySettings.WAYPOINTS_MODE || (BDArmorySettings.RUNWAY_PROJECT && (BDArmorySettings.RUNWAY_PROJECT_ROUND == 50 || BDArmorySettings.RUNWAY_PROJECT_ROUND == 55))) // S4R10 { - if (GUI.Button(SLineRect(++line), (BDArmorySettings.SHOW_WAYPOINTS_OPTIONS ? "Hide " : "Show ") + Localizer.Format("#LOC_BDArmory_Settings_WaypointsOptions"), BDArmorySettings.SHOW_WAYPOINTS_OPTIONS ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))//Show/hide waypoints section + if (GUI.Button(SLineRect(++line), $"{(BDArmorySettings.SHOW_WAYPOINTS_OPTIONS ? StringUtils.Localize("#LOC_BDArmory_Generic_Hide") : StringUtils.Localize("#LOC_BDArmory_Generic_Show"))} {StringUtils.Localize("#LOC_BDArmory_Settings_WaypointsOptions")}", BDArmorySettings.SHOW_WAYPOINTS_OPTIONS ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))//Show/hide waypoints section { BDArmorySettings.SHOW_WAYPOINTS_OPTIONS = !BDArmorySettings.SHOW_WAYPOINTS_OPTIONS; } if (BDArmorySettings.SHOW_WAYPOINTS_OPTIONS) { - // FIXME This is a hack to interface with the Waypoint Spawn Strategy, which isn't written to play nice with local usage. - GUI.Label(SLeftSliderRect(++line), $"Waypoint Altitude: ({BDArmorySettings.WAYPOINTS_ALTITUDE:F0}m)", leftLabel); - BDArmorySettings.WAYPOINTS_ALTITUDE = Utils.RoundToUnit(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.WAYPOINTS_ALTITUDE, 50f, 1000f), 50f); - BDArmorySettings.WAYPOINTS_ONE_AT_A_TIME = GUI.Toggle(SLeftRect(++line), BDArmorySettings.WAYPOINTS_ONE_AT_A_TIME, Localizer.Format("#LOC_BDArmory_Settings_WaypointsOneAtATime")); - BDArmorySettings.WAYPOINTS_VISUALIZE = GUI.Toggle(SLeftRect(++line), BDArmorySettings.WAYPOINTS_VISUALIZE, Localizer.Format("#LOC_BDArmory_Settings_WaypointsShow")); - if (BDArmorySettings.WAYPOINTS_VISUALIZE) + // Select waypoint course + string waypointCourseName; + /* + switch (BDArmorySettings.WAYPOINT_COURSE_INDEX) { + default: + case 0: waypointCourseName = "Canyon"; break; + case 1: waypointCourseName = "Slalom"; break; + case 2: waypointCourseName = "Coastal"; break; + } + */ + waypointCourseName = WaypointCourses.CourseLocations[BDArmorySettings.WAYPOINT_COURSE_INDEX].name; + + GUI.Label(SLeftSliderRect(++line), $"Waypoint Course: ({waypointCourseName})", leftLabel); + BDArmorySettings.WAYPOINT_COURSE_INDEX = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.WAYPOINT_COURSE_INDEX, 0, WaypointCourses.CourseLocations.Count - 1)); - GUI.Label(SLeftSliderRect(++line), $"Waypoint Size: ({BDArmorySettings.WAYPOINTS_SCALE:F0}m)", leftLabel); - BDArmorySettings.WAYPOINTS_SCALE = Utils.RoundToUnit(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.WAYPOINTS_SCALE, 50f, 1000f), 50f); + GUI.Label(SLeftSliderRect(++line), $"Waypoint Altitude: {(BDArmorySettings.WAYPOINTS_ALTITUDE > 0 ? $"({BDArmorySettings.WAYPOINTS_ALTITUDE:F0}m)" : "-Default-")}", leftLabel); + BDArmorySettings.WAYPOINTS_ALTITUDE = BDAMath.RoundToUnit(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.WAYPOINTS_ALTITUDE, 0, 1000f), 50f); + + GUI.Label(SLeftSliderRect(++line), $"Max Laps: {BDArmorySettings.WAYPOINT_LOOP_INDEX:F0}", leftLabel); + BDArmorySettings.WAYPOINT_LOOP_INDEX = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.WAYPOINT_LOOP_INDEX, 1, 5)); + + GUI.Label(SLeftSliderRect(++line), $"Activate Guard After: {(BDArmorySettings.WAYPOINT_GUARD_INDEX < 0 ? "Never" : BDArmorySettings.WAYPOINT_GUARD_INDEX.ToString("F0"))}", leftLabel); + BDArmorySettings.WAYPOINT_GUARD_INDEX = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.WAYPOINT_GUARD_INDEX, -1, WaypointCourses.highestWaypointIndex)); + + if (BDArmorySettings.WAYPOINTS_VISUALIZE) + { + GUI.Label(SLeftSliderRect(++line), $"Waypoint Size: {(BDArmorySettings.WAYPOINTS_SCALE > 0 ? $"({BDArmorySettings.WAYPOINTS_SCALE:F0}m)" : "-Default-")}", leftLabel); + BDArmorySettings.WAYPOINTS_SCALE = BDAMath.RoundToUnit(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.WAYPOINTS_SCALE, 0, 1000f), 50f); if (WaygateCount >= 0) { - GUI.Label(SLeftSliderRect(++line), $"Select Gate Model: " + SelectedModel, leftLabel); - if (SelectedGate != (SelectedGate = Utils.RoundToUnit(GUI.HorizontalSlider(SRightSliderRect(line), SelectedGate, 0, WaygateCount), 1))) + GUI.Label(SLeftSliderRect(++line), $"Select Gate Model: {SelectedModel}", leftLabel); + if (SelectedGate != (SelectedGate = BDAMath.RoundToUnit(GUI.HorizontalSlider(SRightSliderRect(line), SelectedGate, 0, WaygateCount), 1))) { SelectedModel = Path.GetFileNameWithoutExtension(gateFiles[(int)SelectedGate]); } } } + + BDArmorySettings.WAYPOINTS_ONE_AT_A_TIME = GUI.Toggle(SLeftRect(++line), BDArmorySettings.WAYPOINTS_ONE_AT_A_TIME, StringUtils.Localize("#LOC_BDArmory_Settings_WaypointsOneAtATime")); + BDArmorySettings.WAYPOINTS_INFINITE_FUEL_AT_START = GUI.Toggle(SRightRect(line), BDArmorySettings.WAYPOINTS_INFINITE_FUEL_AT_START, StringUtils.Localize("#LOC_BDArmory_Settings_WaypointsInfFuelAtStart")); + BDArmorySettings.WAYPOINTS_VISUALIZE = GUI.Toggle(SLeftRect(++line), BDArmorySettings.WAYPOINTS_VISUALIZE, StringUtils.Localize("#LOC_BDArmory_Settings_WaypointsShow")); } } - if (GUI.Button(SLineRect(++line), (BDArmorySettings.SHOW_TOURNAMENT_OPTIONS ? "Hide " : "Show ") + Localizer.Format("#LOC_BDArmory_Settings_TournamentOptions"), BDArmorySettings.SHOW_TOURNAMENT_OPTIONS ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))//Show/hide tournament options + if (BDArmorySettings.VESSEL_SPAWN_NUMBER_OF_TEAMS != 11) // Tournament options { - BDArmorySettings.SHOW_TOURNAMENT_OPTIONS = !BDArmorySettings.SHOW_TOURNAMENT_OPTIONS; - } - if (BDArmorySettings.SHOW_TOURNAMENT_OPTIONS) - { - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_TournamentDelayBetweenHeats")}: ({BDArmorySettings.TOURNAMENT_DELAY_BETWEEN_HEATS}s)", leftLabel); // Delay between heats - BDArmorySettings.TOURNAMENT_DELAY_BETWEEN_HEATS = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.TOURNAMENT_DELAY_BETWEEN_HEATS, 0f, 15f)); - - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_TournamentTimeWarpBetweenRounds")}: ({(BDArmorySettings.TOURNAMENT_TIMEWARP_BETWEEN_ROUNDS > 0 ? BDArmorySettings.TOURNAMENT_TIMEWARP_BETWEEN_ROUNDS + "min" : "Off")})", leftLabel); // TimeWarp Between Rounds - BDArmorySettings.TOURNAMENT_TIMEWARP_BETWEEN_ROUNDS = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.TOURNAMENT_TIMEWARP_BETWEEN_ROUNDS / 5f, 0f, 72f)) * 5; - - switch (BDArmorySettings.TOURNAMENT_STYLE) + if (GUI.Button(SLineRect(++line), $"{(BDArmorySettings.SHOW_TOURNAMENT_OPTIONS ? StringUtils.Localize("#LOC_BDArmory_Generic_Hide") : StringUtils.Localize("#LOC_BDArmory_Generic_Show"))} {StringUtils.Localize("#LOC_BDArmory_Settings_TournamentOptions")}", BDArmorySettings.SHOW_TOURNAMENT_OPTIONS ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))//Show/hide tournament options { - case 0: - tournamentStyle = "RNG"; - break; - case 1: - tournamentStyle = "N-choose-K"; - break; + BDArmorySettings.SHOW_TOURNAMENT_OPTIONS = !BDArmorySettings.SHOW_TOURNAMENT_OPTIONS; } - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_TournamentStyle")}: ({tournamentStyle})", leftLabel); // Tournament Style - BDArmorySettings.TOURNAMENT_STYLE = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.TOURNAMENT_STYLE, 0f, 1f)); + if (BDArmorySettings.SHOW_TOURNAMENT_OPTIONS) + { + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_TournamentDelayBetweenHeats")}: ({BDArmorySettings.TOURNAMENT_DELAY_BETWEEN_HEATS}s)", leftLabel); // Delay between heats + BDArmorySettings.TOURNAMENT_DELAY_BETWEEN_HEATS = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.TOURNAMENT_DELAY_BETWEEN_HEATS, 0f, 15f)); - var value = BDArmorySettings.TOURNAMENT_ROUNDS <= 20 ? BDArmorySettings.TOURNAMENT_ROUNDS : BDArmorySettings.TOURNAMENT_ROUNDS <= 100 ? (16 + BDArmorySettings.TOURNAMENT_ROUNDS / 5) : 37; - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_TournamentRounds")}: ({BDArmorySettings.TOURNAMENT_ROUNDS})", leftLabel); // Rounds - value = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), value, 1f, 37f)); - BDArmorySettings.TOURNAMENT_ROUNDS = value <= 20 ? value : value <= 36 ? (value - 16) * 5 : BDArmorySettings.TOURNAMENT_ROUNDS_CUSTOM; + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_TournamentTimeWarpBetweenRounds")}: ({(BDArmorySettings.TOURNAMENT_TIMEWARP_BETWEEN_ROUNDS > 0 ? $"{BDArmorySettings.TOURNAMENT_TIMEWARP_BETWEEN_ROUNDS}min" : "Off")})", leftLabel); // TimeWarp Between Rounds + BDArmorySettings.TOURNAMENT_TIMEWARP_BETWEEN_ROUNDS = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.TOURNAMENT_TIMEWARP_BETWEEN_ROUNDS / 5f, 0f, 72f)) * 5; - if (BDArmorySettings.VESSEL_SPAWN_NUMBER_OF_TEAMS == 0) // FFA - { - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_TournamentVesselsPerHeat")}: ({(BDArmorySettings.TOURNAMENT_VESSELS_PER_HEAT > 0 ? BDArmorySettings.TOURNAMENT_VESSELS_PER_HEAT.ToString() : (BDArmorySettings.TOURNAMENT_VESSELS_PER_HEAT == -1 ? "Auto" : "Inf"))})", leftLabel); // Vessels Per Heat - BDArmorySettings.TOURNAMENT_VESSELS_PER_HEAT = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.TOURNAMENT_VESSELS_PER_HEAT, -1f, 20f)); - } - else // Teams - { - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_TournamentTeamsPerHeat")}: ({(BDArmorySettings.TOURNAMENT_TEAMS_PER_HEAT > 1 ? BDArmorySettings.TOURNAMENT_TEAMS_PER_HEAT.ToString() : (BDArmorySettings.TOURNAMENT_TEAMS_PER_HEAT == 0 ? "Auto" : "Inf"))})", leftLabel); // Teams Per Heat - BDArmorySettings.TOURNAMENT_TEAMS_PER_HEAT = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.TOURNAMENT_TEAMS_PER_HEAT, 2f, 8f)); + switch (BDArmorySettings.TOURNAMENT_STYLE) + { + case 0: + tournamentStyle = "RNG"; + break; + case 1: + tournamentStyle = "N-choose-K"; + break; + case 2: + tournamentStyle = "Gauntlet"; + break; + } + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_TournamentStyle")}: ({tournamentStyle})", leftLabel); // Tournament Style + BDArmorySettings.TOURNAMENT_STYLE = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.TOURNAMENT_STYLE, 0f, 2f)); - GUI.Label(SLeftSliderRect(++line), $"{Localizer.Format("#LOC_BDArmory_Settings_TournamentVesselsPerTeam")}: ({BDArmorySettings.TOURNAMENT_VESSELS_PER_TEAM.ToString()})", leftLabel); // Vessels Per Team - BDArmorySettings.TOURNAMENT_VESSELS_PER_TEAM = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.TOURNAMENT_VESSELS_PER_TEAM, 1f, 8f)); + var value = BDArmorySettings.TOURNAMENT_ROUNDS <= 20 ? BDArmorySettings.TOURNAMENT_ROUNDS : BDArmorySettings.TOURNAMENT_ROUNDS <= 100 ? (16 + BDArmorySettings.TOURNAMENT_ROUNDS / 5) : 37; + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_TournamentRounds")}: ({BDArmorySettings.TOURNAMENT_ROUNDS})", leftLabel); // Rounds + value = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), value, 1f, 37f)); + BDArmorySettings.TOURNAMENT_ROUNDS = value <= 20 ? value : value <= 36 ? (value - 16) * 5 : BDArmorySettings.TOURNAMENT_ROUNDS_CUSTOM; - BDArmorySettings.TOURNAMENT_FULL_TEAMS = GUI.Toggle(SLeftRect(++line), BDArmorySettings.TOURNAMENT_FULL_TEAMS, Localizer.Format("#LOC_BDArmory_Settings_TournamentFullTeams")); // Re-use craft to fill teams - } + if (BDArmorySettings.VESSEL_SPAWN_NUMBER_OF_TEAMS == 0) // FFA + { + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_TournamentVesselsPerHeat")}: ({(BDArmorySettings.TOURNAMENT_VESSELS_PER_HEAT > 0 ? BDArmorySettings.TOURNAMENT_VESSELS_PER_HEAT.ToString() : (BDArmorySettings.TOURNAMENT_VESSELS_PER_HEAT == -1 ? "Auto" : "Inf"))})", leftLabel); // Vessels Per Heat + BDArmorySettings.TOURNAMENT_VESSELS_PER_HEAT = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.TOURNAMENT_VESSELS_PER_HEAT, -1f, 20f)); + } + else // Teams + { + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_TournamentTeamsPerHeat")}: ({BDArmorySettings.TOURNAMENT_TEAMS_PER_HEAT})", leftLabel); // Teams Per Heat + BDArmorySettings.TOURNAMENT_TEAMS_PER_HEAT = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.TOURNAMENT_TEAMS_PER_HEAT, BDArmorySettings.TOURNAMENT_STYLE == 2 ? 1f : 2f, 8f)); - // Tournament status - if (BDATournament.Instance.tournamentType == TournamentType.FFA) - { - GUI.Label(SLineRect(++line), $"ID: {BDATournament.Instance.tournamentID}, {BDATournament.Instance.vesselCount} vessels, {BDATournament.Instance.numberOfRounds} rounds, {BDATournament.Instance.numberOfHeats} heats per round ({BDATournament.Instance.heatsRemaining} remaining).", leftLabel); + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_TournamentVesselsPerTeam")}: ({BDArmorySettings.TOURNAMENT_VESSELS_PER_TEAM})", leftLabel); // Vessels Per Team + BDArmorySettings.TOURNAMENT_VESSELS_PER_TEAM = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.TOURNAMENT_VESSELS_PER_TEAM, 1f, 8f)); + + BDArmorySettings.TOURNAMENT_FULL_TEAMS = GUI.Toggle(SLeftRect(++line), BDArmorySettings.TOURNAMENT_FULL_TEAMS, StringUtils.Localize("#LOC_BDArmory_Settings_TournamentFullTeams")); // Re-use craft to fill teams + } + + if (BDArmorySettings.TOURNAMENT_STYLE == 2) // Gauntlet settings + { + GUI.Label(SLeftRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_GauntletOpponentsFilesLocation")} (AutoSpawn{Path.DirectorySeparatorChar}): ", leftLabel); // Gauntlet opponent craft files location + BDArmorySettings.VESSEL_SPAWN_GAUNTLET_OPPONENTS_FILES_LOCATION = GUI.TextField(SRightRect(line), BDArmorySettings.VESSEL_SPAWN_GAUNTLET_OPPONENTS_FILES_LOCATION); + + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_TournamentOpponentTeamsPerHeat")}: ({BDArmorySettings.TOURNAMENT_OPPONENT_TEAMS_PER_HEAT})", leftLabel); // Opponent Teams Per Heat + BDArmorySettings.TOURNAMENT_OPPONENT_TEAMS_PER_HEAT = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.TOURNAMENT_OPPONENT_TEAMS_PER_HEAT, 1f, 8f)); + + GUI.Label(SLeftSliderRect(++line), $"{StringUtils.Localize("#LOC_BDArmory_Settings_TournamentOpponentVesselsPerTeam")}: ({BDArmorySettings.TOURNAMENT_OPPONENT_VESSELS_PER_TEAM})", leftLabel); // Opponent Vessels Per Team + BDArmorySettings.TOURNAMENT_OPPONENT_VESSELS_PER_TEAM = Mathf.RoundToInt(GUI.HorizontalSlider(SRightSliderRect(line), BDArmorySettings.TOURNAMENT_OPPONENT_VESSELS_PER_TEAM, 1f, 8f)); + } + else { BDArmorySettings.TOURNAMENT_TEAMS_PER_HEAT = Math.Max(2, BDArmorySettings.TOURNAMENT_TEAMS_PER_HEAT); } + + // Tournament status + if (BDATournament.Instance.tournamentType == TournamentType.FFA) + { + GUI.Label(SLineRect(++line), $"ID: {BDATournament.Instance.tournamentID}, {BDATournament.Instance.vesselCount} vessels, {BDATournament.Instance.numberOfRounds} rounds, {BDATournament.Instance.numberOfHeats} heats per round ({BDATournament.Instance.heatsRemaining} remaining).", leftLabel); + } + else + { + GUI.Label(SLineRect(++line), $"ID: {BDATournament.Instance.tournamentID}, {BDATournament.Instance.teamCount} teams, {BDATournament.Instance.numberOfRounds} rounds, {BDATournament.Instance.teamsPerHeat} teams per heat, {BDATournament.Instance.numberOfHeats} heats per round,", leftLabel); + GUI.Label(SLineRect(++line), $"{BDATournament.Instance.vesselCount} vessels,{(BDATournament.Instance.fullTeams ? "" : " up to")} {BDATournament.Instance.vesselsPerTeam} vessels per team per heat, {BDATournament.Instance.heatsRemaining} heats remaining.", leftLabel); + } + switch (BDATournament.Instance.tournamentStatus) + { + case TournamentStatus.Running: + case TournamentStatus.Waiting: + if (GUI.Button(SLeftRect(++line), StringUtils.Localize("#LOC_BDArmory_Settings_TournamentStop"), BDArmorySetup.BDGuiSkin.button)) // Stop tournament + BDATournament.Instance.StopTournament(); + GUI.Label(SRightRect(line), $" Status: {BDATournament.Instance.tournamentStatus}, Round {BDATournament.Instance.currentRound}, Heat {BDATournament.Instance.currentHeat}"); + break; + + default: + if (GUI.Button(SLeftRect(++line), StringUtils.Localize("#LOC_BDArmory_Settings_TournamentSetup"), BDArmorySetup.BDGuiSkin.button)) // Setup tournament + { + ParseAllSpawnFieldsNow(); + BDATournament.Instance.SetupTournament( + BDArmorySettings.VESSEL_SPAWN_FILES_LOCATION, + BDArmorySettings.TOURNAMENT_ROUNDS, + BDArmorySettings.TOURNAMENT_VESSELS_PER_HEAT, + BDArmorySettings.TOURNAMENT_TEAMS_PER_HEAT, + BDArmorySettings.TOURNAMENT_VESSELS_PER_TEAM, + BDArmorySettings.VESSEL_SPAWN_NUMBER_OF_TEAMS, + BDArmorySettings.TOURNAMENT_STYLE + ); + BDArmorySetup.SaveConfig(); + } + + if (BDATournament.Instance.tournamentStatus != TournamentStatus.Completed) + { + if (GUI.Button(SRightRect(line), StringUtils.Localize("#LOC_BDArmory_Settings_TournamentRun"), BDArmorySetup.BDGuiSkin.button)) // Run tournament + { + _vesselsSpawned = false; + SpawnUtils.CancelSpawning(); // Stop any spawning that's currently happening. + BDATournament.Instance.RunTournament(); + if (BDArmorySettings.VESSEL_SPAWNER_WINDOW_WIDTH < 480 && BDATournament.Instance.numberOfRounds * BDATournament.Instance.numberOfHeats > 99) // Expand the window a bit to compensate for long tournaments. + { + BDArmorySettings.VESSEL_SPAWNER_WINDOW_WIDTH = 480; + } + } + } + break; + } } - else + } + else // Custom Spawn Template + { + if (GUI.Button(SLineRect(++line), $"{(BDArmorySettings.CUSTOM_SPAWN_TEMPLATE_SHOW_OPTIONS ? StringUtils.Localize("#LOC_BDArmory_Generic_Hide") : StringUtils.Localize("#LOC_BDArmory_Generic_Show"))} {StringUtils.Localize("#LOC_BDArmory_Settings_CustomSpawnTemplateOptions")}", BDArmorySettings.CUSTOM_SPAWN_TEMPLATE_SHOW_OPTIONS ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button))//Show/hide tournament options { - GUI.Label(SLineRect(++line), $"ID: {BDATournament.Instance.tournamentID}, {BDATournament.Instance.teamCount} teams, {BDATournament.Instance.numberOfRounds} rounds, {BDATournament.Instance.teamsPerHeat} teams per heat, {BDATournament.Instance.numberOfHeats} heats per round,", leftLabel); - GUI.Label(SLineRect(++line), $"{BDATournament.Instance.vesselCount} vessels,{(BDATournament.Instance.fullTeams ? "" : " up to")} {BDATournament.Instance.vesselsPerTeam} vessels per team per heat, {BDATournament.Instance.heatsRemaining} heats remaining.", leftLabel); + BDArmorySettings.CUSTOM_SPAWN_TEMPLATE_SHOW_OPTIONS = !BDArmorySettings.CUSTOM_SPAWN_TEMPLATE_SHOW_OPTIONS; } - switch (BDATournament.Instance.tournamentStatus) + if (BDArmorySettings.CUSTOM_SPAWN_TEMPLATE_SHOW_OPTIONS) { - case TournamentStatus.Running: - case TournamentStatus.Waiting: - if (GUI.Button(SLeftRect(++line), Localizer.Format("#LOC_BDArmory_Settings_TournamentStop"), BDArmorySetup.BDGuiSkin.button)) // Stop tournament - BDATournament.Instance.StopTournament(); - GUI.Label(SRightRect(line), $" Status: {BDATournament.Instance.tournamentStatus}, Round {BDATournament.Instance.currentRound}, Heat {BDATournament.Instance.currentHeat}"); - break; - - default: - if (GUI.Button(SLeftRect(++line), Localizer.Format("#LOC_BDArmory_Settings_TournamentSetup"), BDArmorySetup.BDGuiSkin.button)) // Setup tournament - { - ParseAllSpawnFieldsNow(); - BDATournament.Instance.SetupTournament( - BDArmorySettings.VESSEL_SPAWN_FILES_LOCATION, - BDArmorySettings.TOURNAMENT_ROUNDS, - BDArmorySettings.TOURNAMENT_VESSELS_PER_HEAT, - BDArmorySettings.TOURNAMENT_TEAMS_PER_HEAT, - BDArmorySettings.TOURNAMENT_VESSELS_PER_TEAM, - BDArmorySettings.VESSEL_SPAWN_NUMBER_OF_TEAMS, - BDArmorySettings.TOURNAMENT_STYLE - ); - BDArmorySetup.SaveConfig(); - } - - if (BDATournament.Instance.tournamentStatus != TournamentStatus.Completed) + line += 0.25f; + var spawnTemplate = CustomTemplateSpawning.Instance.customSpawnConfig; + spawnTemplate.name = GUIUtils.TextField(spawnTemplate.name, "Specify a name then save the template.", rect:SQuarterRect(++line, 0, 2)); // Writing in the text field updates the name of the current template. + if (GUI.Button(SQuarterRect(line, 2), StringUtils.Localize("#LOC_BDArmory_Generic_Load"), BDArmorySetup.BDGuiSkin.button)) + { + CustomTemplateSpawning.Instance.ShowTemplateSelection(Event.current.mousePosition + BDArmorySetup.WindowRectVesselSpawner.position); + } + if (GUI.Button(SEighthRect(line, 6), StringUtils.Localize("#LOC_BDArmory_Generic_Save"), BDArmorySetup.BDGuiSkin.button)) // Save overwrites the current template with the current vessel positions in the LoadedVesselSwitcher. + { + CustomTemplateSpawning.Instance.SaveTemplate(); + } + if (GUI.Button(SEighthRect(line, 7), StringUtils.Localize("#LOC_BDArmory_Generic_New"), BDArmorySetup.BDGuiSkin.button)) // New generates a new template from the current vessels in the LoadedVesselSwitcher. + { + spawnTemplate = CustomTemplateSpawning.Instance.NewTemplate(); + } + line += 0.25f; + // We then want a table of teams of craft buttons for selecting the craft with kerbal buttons beside them for selecting the kerbals. + char teamName = 'A'; + foreach (var team in spawnTemplate.customVesselSpawnConfigs) + { + foreach (var member in team) { - if (GUI.Button(SRightRect(line), Localizer.Format("#LOC_BDArmory_Settings_TournamentRun"), BDArmorySetup.BDGuiSkin.button)) // Run tournament + GUI.Label(ShortLabel(++line, 20), $"{teamName}: "); + // if (GUI.Button(SQuarterRect(line, 0, 3, 20), Path.GetFileNameWithoutExtension(member.craftURL), BDArmorySetup.BDGuiSkin.button)) + if (GUI.Button(SQuarterRect(line, 0, 3, 20), CustomTemplateSpawning.Instance.ShipName(member.craftURL), BDArmorySetup.BDGuiSkin.button)) { - BDATournament.Instance.RunTournament(); - if (BDArmorySettings.VESSEL_SPAWNER_WINDOW_WIDTH < 480 && BDATournament.Instance.numberOfRounds * BDATournament.Instance.numberOfHeats > 99) // Expand the window a bit to compensate for long tournaments. - { - Debug.Log("DEBUG widening window"); - BDArmorySettings.VESSEL_SPAWNER_WINDOW_WIDTH = 480; - } + if (Event.current.button == 1)//Right click + CustomTemplateSpawning.Instance.HideVesselSelection(member); + else + CustomTemplateSpawning.Instance.ShowVesselSelection(Event.current.mousePosition + BDArmorySetup.WindowRectVesselSpawner.position, member, team); + } + if (GUI.Button(SQuarterRect(line, 3, 1), string.IsNullOrEmpty(member.kerbalName) ? "random" : member.kerbalName, BDArmorySetup.BDGuiSkin.button)) + { + if (Event.current.button == 1) // Right click + CustomTemplateSpawning.Instance.HideCrewSelection(member); + else + CustomTemplateSpawning.Instance.ShowCrewSelection(Event.current.mousePosition + BDArmorySetup.WindowRectVesselSpawner.position, member); } } - break; + ++teamName; + line += 0.25f; + } + --line; } } - ++line; - if (BDArmorySettings.WAYPOINTS_MODE || (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 50)) // S4R10 + if (BDArmorySettings.WAYPOINTS_MODE || (BDArmorySettings.RUNWAY_PROJECT && (BDArmorySettings.RUNWAY_PROJECT_ROUND == 50 || BDArmorySettings.RUNWAY_PROJECT_ROUND == 55))) // S4R10 { if (GUI.Button(SLineRect(++line), "Run waypoints", BDArmorySetup.BDGuiSkin.button)) { + BDATournament.Instance.StopTournament(); if (TournamentCoordinator.Instance.IsRunning) // Stop either case. { TournamentCoordinator.Instance.Stop(); TournamentCoordinator.Instance.StopForEach(); } + float spawnLatitude, spawnLongitude; + List course; //adapt to how the spawn locations are displayed/selected + //add new GUI window for waypoint course creation; new name entry field for course, waypoints, save button to save WP coords + //Spawn button to spawn in WP (+ WP visualizer); movement buttons/widget for moving Wp around (+ fineness slider to set increment amount); have these display to a numeric field for numfield editing instead? + /* + switch (BDArmorySettings.WAYPOINT_COURSE_INDEX) + { + default: + case 1: + //spawnLocation.location; + spawnLatitude = WaypointCourses.CourseLocations[0].spawnPoint.x; + spawnLongitude = WaypointCourses.CourseLocations[0].spawnPoint.y; + course = WaypointCourses.CourseLocations[0].waypoints; + break; + case 2: + spawnLatitude = WaypointCourses.CourseLocations[1].spawnPoint.x; + spawnLongitude = WaypointCourses.CourseLocations[1].spawnPoint.y; + course = WaypointCourses.CourseLocations[1].waypoints; + break; + case 3: + spawnLatitude = WaypointCourses.CourseLocations[2].spawnPoint.x; + spawnLongitude = WaypointCourses.CourseLocations[2].spawnPoint.y; + course = WaypointCourses.CourseLocations[2].waypoints; + break; + } + */ + spawnLatitude = WaypointCourses.CourseLocations[BDArmorySettings.WAYPOINT_COURSE_INDEX].spawnPoint.x; + spawnLongitude = WaypointCourses.CourseLocations[BDArmorySettings.WAYPOINT_COURSE_INDEX].spawnPoint.y; + course = WaypointCourses.CourseLocations[BDArmorySettings.WAYPOINT_COURSE_INDEX].waypoints; + if (!BDArmorySettings.WAYPOINTS_ONE_AT_A_TIME) { TournamentCoordinator.Instance.Configure(new SpawnConfigStrategy( - new SpawnConfig( - 1, - 27.97f,// BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.x, - -39.35f,// BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.y, - BDArmorySettings.VESSEL_SPAWN_ALTITUDE, + new CircularSpawnConfig( + new SpawnConfig( + Event.current.button == 1 ? BDArmorySettings.VESSEL_SPAWN_WORLDINDEX : WaypointCourses.CourseLocations[BDArmorySettings.WAYPOINT_COURSE_INDEX].worldIndex, // Right-click => use the VesselSpawnerWindow settings instead of the defaults. + Event.current.button == 1 ? BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.x : spawnLatitude, + Event.current.button == 1 ? BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.y : spawnLongitude, + BDArmorySettings.VESSEL_SPAWN_ALTITUDE, + true, + BDArmorySettings.VESSEL_SPAWN_REASSIGN_TEAMS, + BDArmorySettings.VESSEL_SPAWN_NUMBER_OF_TEAMS, + null, + null, + BDArmorySettings.VESSEL_SPAWN_FILES_LOCATION + ), BDArmorySettings.VESSEL_SPAWN_DISTANCE_TOGGLE ? BDArmorySettings.VESSEL_SPAWN_DISTANCE : BDArmorySettings.VESSEL_SPAWN_DISTANCE_FACTOR, - BDArmorySettings.VESSEL_SPAWN_DISTANCE_TOGGLE, - BDArmorySettings.VESSEL_SPAWN_EASE_IN_SPEED, - true, - BDArmorySettings.VESSEL_SPAWN_REASSIGN_TEAMS, - BDArmorySettings.VESSEL_SPAWN_NUMBER_OF_TEAMS, - null, - null, - BDArmorySettings.VESSEL_SPAWN_FILES_LOCATION) - ), - new WaypointFollowingStrategy( - new List { - new WaypointFollowingStrategy.Waypoint(28.33f, -39.11f, BDArmorySettings.WAYPOINTS_ALTITUDE), - new WaypointFollowingStrategy.Waypoint(28.83f, -38.06f, BDArmorySettings.WAYPOINTS_ALTITUDE), - new WaypointFollowingStrategy.Waypoint(29.54f, -38.68f, BDArmorySettings.WAYPOINTS_ALTITUDE), - new WaypointFollowingStrategy.Waypoint(30.15f, -38.6f, BDArmorySettings.WAYPOINTS_ALTITUDE), - new WaypointFollowingStrategy.Waypoint(30.83f, -38.87f, BDArmorySettings.WAYPOINTS_ALTITUDE), - new WaypointFollowingStrategy.Waypoint(30.73f, -39.6f, BDArmorySettings.WAYPOINTS_ALTITUDE), - new WaypointFollowingStrategy.Waypoint(30.9f, -40.23f, BDArmorySettings.WAYPOINTS_ALTITUDE), - new WaypointFollowingStrategy.Waypoint(30.83f, -41.26f, BDArmorySettings.WAYPOINTS_ALTITUDE) - } + BDArmorySettings.VESSEL_SPAWN_DISTANCE_TOGGLE) ), + new WaypointFollowingStrategy(course), CircularSpawning.Instance ); @@ -672,69 +834,142 @@ private void WindowVesselSpawner(int id) { var craftFiles = Directory.GetFiles(Path.Combine(KSPUtil.ApplicationRootPath, "AutoSpawn", BDArmorySettings.VESSEL_SPAWN_FILES_LOCATION), "*.craft").ToList(); var strategies = craftFiles.Select(craftFile => new SpawnConfigStrategy( - new SpawnConfig( - 1, - 27.97f,// BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.x, - -39.35f,// BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.y, - BDArmorySettings.VESSEL_SPAWN_ALTITUDE, + new CircularSpawnConfig( + new SpawnConfig( + Event.current.button == 1 ? BDArmorySettings.VESSEL_SPAWN_WORLDINDEX : WaypointCourses.CourseLocations[BDArmorySettings.WAYPOINT_COURSE_INDEX].worldIndex, + Event.current.button == 1 ? BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.x : spawnLatitude, + Event.current.button == 1 ? BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.y : spawnLongitude, + BDArmorySettings.VESSEL_SPAWN_ALTITUDE, + true, + BDArmorySettings.VESSEL_SPAWN_REASSIGN_TEAMS, + 0, // This should always be 0 (FFA) to avoid the logic for spawning teams in one-at-a-time mode. + null, + null, + null, + new List() { craftFile } + ), BDArmorySettings.VESSEL_SPAWN_DISTANCE_TOGGLE ? BDArmorySettings.VESSEL_SPAWN_DISTANCE : BDArmorySettings.VESSEL_SPAWN_DISTANCE_FACTOR, - BDArmorySettings.VESSEL_SPAWN_DISTANCE_TOGGLE, - BDArmorySettings.VESSEL_SPAWN_EASE_IN_SPEED, - true, - BDArmorySettings.VESSEL_SPAWN_REASSIGN_TEAMS, - BDArmorySettings.VESSEL_SPAWN_NUMBER_OF_TEAMS, - null, - null, - null, - new List() { craftFile } + BDArmorySettings.VESSEL_SPAWN_DISTANCE_TOGGLE ))).ToList(); TournamentCoordinator.Instance.RunForEach(strategies, - new WaypointFollowingStrategy( - new List { - new WaypointFollowingStrategy.Waypoint(28.33f, -39.11f, BDArmorySettings.WAYPOINTS_ALTITUDE), - new WaypointFollowingStrategy.Waypoint(28.83f, -38.06f, BDArmorySettings.WAYPOINTS_ALTITUDE), - new WaypointFollowingStrategy.Waypoint(29.54f, -38.68f, BDArmorySettings.WAYPOINTS_ALTITUDE), - new WaypointFollowingStrategy.Waypoint(30.15f, -38.6f, BDArmorySettings.WAYPOINTS_ALTITUDE), - new WaypointFollowingStrategy.Waypoint(30.83f, -38.87f, BDArmorySettings.WAYPOINTS_ALTITUDE), - new WaypointFollowingStrategy.Waypoint(30.73f, -39.6f, BDArmorySettings.WAYPOINTS_ALTITUDE), - new WaypointFollowingStrategy.Waypoint(30.9f, -40.23f, BDArmorySettings.WAYPOINTS_ALTITUDE), - new WaypointFollowingStrategy.Waypoint(30.83f, -41.26f, BDArmorySettings.WAYPOINTS_ALTITUDE) - } - ), + new WaypointFollowingStrategy(course), CircularSpawning.Instance ); } } } + else if (BDArmorySettings.VESSEL_SPAWN_NUMBER_OF_TEAMS == 11) // Custom Spawn Template + { + if (BDACompetitionMode.Instance.competitionIsActive || BDACompetitionMode.Instance.competitionStarting) + { + if (GUI.Button(SLineRect(++line), StringUtils.Localize("#LOC_BDArmory_Settings_StopCompetition"), BDArmorySetup.BDGuiSkin.box)) // Stop competition. + BDACompetitionMode.Instance.StopCompetition(); + } + else + { + var spawnAndStartCompetition = GUI.Button(SLeftButtonRect(++line), StringUtils.Localize("#LOC_BDArmory_Settings_SpawnAndStartCompetition"), BDArmorySetup.BDGuiSkin.button); + var spawnOnly = GUI.Button(SRightButtonRect(line), StringUtils.Localize("#LOC_BDArmory_Settings_SpawnOnly"), BDArmorySetup.BDGuiSkin.button); + if (spawnOnly || spawnAndStartCompetition) + { + // Stop any currently running tournament. + BDATournament.Instance.StopTournament(); + if (TournamentCoordinator.Instance.IsRunning) + { + TournamentCoordinator.Instance.Stop(); + TournamentCoordinator.Instance.StopForEach(); + } + // Configure the current custom spawn template. + if (CustomTemplateSpawning.Instance.ConfigureTemplate(spawnAndStartCompetition)) + { + // Spawn the craft and start the competition. + CustomTemplateSpawning.Instance.SpawnCustomTemplate(CustomTemplateSpawning.Instance.customSpawnConfig); + } + } + } + } else { - if (GUI.Button(SLineRect(++line), Localizer.Format("#LOC_BDArmory_Settings_SingleSpawn"), _vesselsSpawned ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button)) + if (GUI.Button(SLineRect(++line), StringUtils.Localize("#LOC_BDArmory_Settings_SingleSpawn"), _vesselsSpawned ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button)) { BDATournament.Instance.StopTournament(); ParseAllSpawnFieldsNow(); if (!_vesselsSpawned && !ContinuousSpawning.Instance.vesselsSpawningContinuously && Event.current.button == 0) // Left click { if (BDArmorySettings.VESSEL_SPAWN_CONTINUE_SINGLE_SPAWNING) - CircularSpawning.Instance.SpawnAllVesselsOnceContinuously(BDArmorySettings.VESSEL_SPAWN_WORLDINDEX, BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.x, BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.y, BDArmorySettings.VESSEL_SPAWN_ALTITUDE, BDArmorySettings.VESSEL_SPAWN_DISTANCE_TOGGLE ? BDArmorySettings.VESSEL_SPAWN_DISTANCE : BDArmorySettings.VESSEL_SPAWN_DISTANCE_FACTOR, BDArmorySettings.VESSEL_SPAWN_DISTANCE_TOGGLE, BDArmorySettings.VESSEL_SPAWN_EASE_IN_SPEED, true, BDArmorySettings.VESSEL_SPAWN_REASSIGN_TEAMS, BDArmorySettings.VESSEL_SPAWN_NUMBER_OF_TEAMS, null, null, BDArmorySettings.VESSEL_SPAWN_FILES_LOCATION); // Spawn vessels. + CircularSpawning.Instance.SpawnAllVesselsOnceContinuously( + BDArmorySettings.VESSEL_SPAWN_WORLDINDEX, + BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.x, + BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.y, + BDArmorySettings.VESSEL_SPAWN_ALTITUDE, + BDArmorySettings.VESSEL_SPAWN_DISTANCE_TOGGLE ? BDArmorySettings.VESSEL_SPAWN_DISTANCE : BDArmorySettings.VESSEL_SPAWN_DISTANCE_FACTOR, + BDArmorySettings.VESSEL_SPAWN_DISTANCE_TOGGLE, + true, + BDArmorySettings.VESSEL_SPAWN_REASSIGN_TEAMS, + BDArmorySettings.VESSEL_SPAWN_NUMBER_OF_TEAMS, + null, + null, + BDArmorySettings.VESSEL_SPAWN_FILES_LOCATION + ); // Spawn vessels. else - CircularSpawning.Instance.SpawnAllVesselsOnce(BDArmorySettings.VESSEL_SPAWN_WORLDINDEX, BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.x, BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.y, BDArmorySettings.VESSEL_SPAWN_ALTITUDE, BDArmorySettings.VESSEL_SPAWN_DISTANCE_TOGGLE ? BDArmorySettings.VESSEL_SPAWN_DISTANCE : BDArmorySettings.VESSEL_SPAWN_DISTANCE_FACTOR, BDArmorySettings.VESSEL_SPAWN_DISTANCE_TOGGLE, BDArmorySettings.VESSEL_SPAWN_EASE_IN_SPEED, true, BDArmorySettings.VESSEL_SPAWN_REASSIGN_TEAMS, BDArmorySettings.VESSEL_SPAWN_NUMBER_OF_TEAMS, null, null, BDArmorySettings.VESSEL_SPAWN_FILES_LOCATION); // Spawn vessels. + { + CircularSpawning.Instance.SpawnAllVesselsOnce( + BDArmorySettings.VESSEL_SPAWN_WORLDINDEX, + BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.x, + BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.y, + BDArmorySettings.VESSEL_SPAWN_ALTITUDE_, + BDArmorySettings.VESSEL_SPAWN_DISTANCE_TOGGLE ? BDArmorySettings.VESSEL_SPAWN_DISTANCE : BDArmorySettings.VESSEL_SPAWN_DISTANCE_FACTOR, + BDArmorySettings.VESSEL_SPAWN_DISTANCE_TOGGLE, + true, + BDArmorySettings.VESSEL_SPAWN_REASSIGN_TEAMS, + BDArmorySettings.VESSEL_SPAWN_NUMBER_OF_TEAMS, + null, + null, + BDArmorySettings.VESSEL_SPAWN_FILES_LOCATION + ); // Spawn vessels. + if (BDArmorySettings.VESSEL_SPAWN_START_COMPETITION_AUTOMATICALLY) + StartCoroutine(StartCompetitionOnceSpawned()); + } _vesselsSpawned = true; } else if (Event.current.button == 2) // Middle click, add a new spawn of vessels to the currently spawned vessels. { - CircularSpawning.Instance.SpawnAllVesselsOnce(BDArmorySettings.VESSEL_SPAWN_WORLDINDEX, BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.x, BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.y, BDArmorySettings.VESSEL_SPAWN_ALTITUDE, BDArmorySettings.VESSEL_SPAWN_DISTANCE_TOGGLE ? BDArmorySettings.VESSEL_SPAWN_DISTANCE : BDArmorySettings.VESSEL_SPAWN_DISTANCE_FACTOR, BDArmorySettings.VESSEL_SPAWN_DISTANCE_TOGGLE, BDArmorySettings.VESSEL_SPAWN_EASE_IN_SPEED, false, false, 0, null, null, BDArmorySettings.VESSEL_SPAWN_FILES_LOCATION); // Spawn vessels, without killing off other vessels or changing camera positions. + CircularSpawning.Instance.SpawnAllVesselsOnce( + BDArmorySettings.VESSEL_SPAWN_WORLDINDEX, + BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.x, + BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.y, + BDArmorySettings.VESSEL_SPAWN_ALTITUDE, + BDArmorySettings.VESSEL_SPAWN_DISTANCE_TOGGLE ? BDArmorySettings.VESSEL_SPAWN_DISTANCE : BDArmorySettings.VESSEL_SPAWN_DISTANCE_FACTOR, + BDArmorySettings.VESSEL_SPAWN_DISTANCE_TOGGLE, + false, + false, + 0, + null, + null, + BDArmorySettings.VESSEL_SPAWN_FILES_LOCATION + ); // Spawn vessels, without killing off other vessels or changing camera positions. } } - if (GUI.Button(SLineRect(++line), Localizer.Format("#LOC_BDArmory_Settings_ContinuousSpawning"), ContinuousSpawning.Instance.vesselsSpawningContinuously ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button)) + if (GUI.Button(SLineRect(++line), StringUtils.Localize("#LOC_BDArmory_Settings_ContinuousSpawning"), ContinuousSpawning.Instance.vesselsSpawningContinuously ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button)) { BDATournament.Instance.StopTournament(); ParseAllSpawnFieldsNow(); if (!ContinuousSpawning.Instance.vesselsSpawningContinuously && !_vesselsSpawned && Event.current.button == 0) // Left click { - ContinuousSpawning.Instance.SpawnVesselsContinuously(BDArmorySettings.VESSEL_SPAWN_WORLDINDEX, BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.x, BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.y, BDArmorySettings.VESSEL_SPAWN_ALTITUDE, BDArmorySettings.VESSEL_SPAWN_DISTANCE_TOGGLE ? BDArmorySettings.VESSEL_SPAWN_DISTANCE : BDArmorySettings.VESSEL_SPAWN_DISTANCE_FACTOR, BDArmorySettings.VESSEL_SPAWN_DISTANCE_TOGGLE, true, BDArmorySettings.VESSEL_SPAWN_FILES_LOCATION); // Spawn vessels continuously at 1km above terrain. + ContinuousSpawning.Instance.SpawnVesselsContinuously( + new CircularSpawnConfig( + new SpawnConfig( + BDArmorySettings.VESSEL_SPAWN_WORLDINDEX, + BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.x, BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.y, BDArmorySettings.VESSEL_SPAWN_ALTITUDE_, + true, true, 1, null, null, + BDArmorySettings.VESSEL_SPAWN_FILES_LOCATION + ), + BDArmorySettings.VESSEL_SPAWN_DISTANCE_TOGGLE ? BDArmorySettings.VESSEL_SPAWN_DISTANCE : BDArmorySettings.VESSEL_SPAWN_DISTANCE_FACTOR, + BDArmorySettings.VESSEL_SPAWN_DISTANCE_TOGGLE + ) + ); // Spawn vessels continuously at 1km above terrain. } } - if (GUI.Button(SLineRect(++line), Localizer.Format("#LOC_BDArmory_Settings_CancelSpawning"), (_vesselsSpawned || ContinuousSpawning.Instance.vesselsSpawningContinuously) ? BDArmorySetup.BDGuiSkin.button : BDArmorySetup.BDGuiSkin.box)) + if (GUI.Button(SLineRect(++line), StringUtils.Localize("#LOC_BDArmory_Settings_CancelSpawning"), (_vesselsSpawned || ContinuousSpawning.Instance.vesselsSpawningContinuously) ? BDArmorySetup.BDGuiSkin.button : BDArmorySetup.BDGuiSkin.box)) { if (_vesselsSpawned) Debug.Log("[BDArmory.VesselSpawnerWindow]: Resetting spawning vessel button."); @@ -745,9 +980,142 @@ private void WindowVesselSpawner(int id) SpawnUtils.CancelSpawning(); } } + // #if DEBUG + // if (BDArmorySettings.DEBUG_SPAWNING && GUI.Button(SLineRect(++line), "Test point spawn", BDArmorySetup.BDGuiSkin.button)) + // { + // StartCoroutine(SingleVesselSpawning.Instance.Spawn( + // new CircularSpawnConfig( + // new SpawnConfig( + // BDArmorySettings.VESSEL_SPAWN_WORLDINDEX, + // BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.x, + // BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.y, + // BDArmorySettings.VESSEL_SPAWN_ALTITUDE, + // false, + // false, + // 0, + // null, + // null, + // BDArmorySettings.VESSEL_SPAWN_FILES_LOCATION + // ), + // BDArmorySettings.VESSEL_SPAWN_DISTANCE_TOGGLE ? BDArmorySettings.VESSEL_SPAWN_DISTANCE : BDArmorySettings.VESSEL_SPAWN_DISTANCE_FACTOR, + // BDArmorySettings.VESSEL_SPAWN_DISTANCE_TOGGLE + // ) + // )); + // } + // #endif line += 1.25f; // Bottom internal margin _windowHeight = (line * _lineHeight); } + + IEnumerator StartCompetitionOnceSpawned() + { + yield return new WaitWhile(() => VesselSpawnerStatus.vesselsSpawning); + if (!VesselSpawnerStatus.vesselSpawnSuccess) yield break; + if (BDArmorySettings.RUNWAY_PROJECT) + { + switch (BDArmorySettings.RUNWAY_PROJECT_ROUND) + { + case 33: + BDACompetitionMode.Instance.StartRapidDeployment(0); + yield break; + case 44: + BDACompetitionMode.Instance.StartRapidDeployment(0); + yield break; + case 53: + BDACompetitionMode.Instance.StartRapidDeployment(0); + yield break; + } + } + BDACompetitionMode.Instance.StartCompetitionMode(BDArmorySettings.COMPETITION_DISTANCE, BDArmorySettings.COMPETITION_START_DESPITE_FAILURES); + } + + public void SetVisible(bool visible) + { + BDArmorySetup.showVesselSpawnerGUI = visible; + GUIUtils.SetGUIRectVisible(_guiCheckIndex, visible); + } + + #region Observers + static int _observerGUICheckIndex = -1; + bool showObserverWindow = false; + bool bringObserverWindowToFront = false; + bool potentialObserversNeedsRefreshing = false; + Rect observerWindowRect = new Rect(0, 0, 300, 250); + Vector2 observerSelectionScrollPos = default; + List potentialObservers = new List(); + public HashSet Observers = new HashSet(); + void ShowObserverWindow(bool show, Vector2 position = default) + { + if (show) + { + observerWindowRect.position = position + new Vector2(50, -observerWindowRect.height / 2); // Centred and slightly offset to allow clicking the same spot. + RefreshObservers(); + bringObserverWindowToFront = true; + } + else + { + potentialObservers.Clear(); + if (BDArmorySettings.VESSEL_SPAWN_NUMBER_OF_TEAMS == 11) // Custom Spawn Template + CustomTemplateSpawning.Instance.RefreshObserverCrewMembers(); + } + showObserverWindow = show; + GUIUtils.SetGUIRectVisible(_observerGUICheckIndex, show); + } + void ObserverWindow(int windowID) + { + GUI.DragWindow(new Rect(0, 0, observerWindowRect.width, 20)); + GUILayout.BeginVertical(); + observerSelectionScrollPos = GUILayout.BeginScrollView(observerSelectionScrollPos, GUI.skin.box, GUILayout.Width(observerWindowRect.width - 15), GUILayout.MaxHeight(observerWindowRect.height - 20)); + int count = 0; + using (var potentialObserver = potentialObservers.GetEnumerator()) + while (potentialObserver.MoveNext()) + { + if (potentialObserver.Current == null) { potentialObserversNeedsRefreshing = true; continue; } + bool isSelected = Observers.Contains(potentialObserver.Current); + if (isSelected) ++count; + if (GUILayout.Button(potentialObserver.Current.vesselName, isSelected ? BDArmorySetup.BDGuiSkin.box : BDArmorySetup.BDGuiSkin.button, GUILayout.Height(30))) + { + if (isSelected) Observers.Remove(potentialObserver.Current); + else Observers.Add(potentialObserver.Current); + } + } + GUILayout.EndScrollView(); + if (count == potentialObservers.Count) + { + if (GUILayout.Button(StringUtils.Localize("#LOC_BDArmory_ObserverSelection_SelectNone"), BDArmorySetup.BDGuiSkin.box, GUILayout.Height(30))) + Observers.Clear(); + } + else + { + if (GUILayout.Button(StringUtils.Localize("#LOC_BDArmory_ObserverSelection_SelectAll"), BDArmorySetup.BDGuiSkin.button, GUILayout.Height(30))) + Observers = potentialObservers.Where(o => o != null).ToHashSet(); + } + GUILayout.EndVertical(); + GUIUtils.RepositionWindow(ref observerWindowRect); + GUIUtils.UpdateGUIRect(observerWindowRect, _observerGUICheckIndex); + GUIUtils.UseMouseEventInRect(observerWindowRect); + if (bringObserverWindowToFront) + { + bringObserverWindowToFront = false; + GUI.BringWindowToFront(windowID); + } + } + void RefreshObservers() + { + potentialObservers.Clear(); + foreach (var vessel in FlightGlobals.Vessels) + { + if (vessel == null) continue; + if (vessel.vesselType == VesselType.Debris || vessel.vesselType == VesselType.SpaceObject) continue; // Ignore debris and space objects. + if (VesselModuleRegistry.GetModuleCount(vessel) > 0 // Check for an AI. + && VesselModuleRegistry.GetModuleCount(vessel) > 0 // Check for a WM. + && vessel.IsControllable + ) continue; // It's an active vessel, skip it. + potentialObservers.Add(vessel); + } + Observers = Observers.Where(o => potentialObservers.Contains(o)).ToHashSet(); + } + #endregion } } diff --git a/BDArmory/Control/AIUtils.cs b/BDArmory/Utils/AIUtils.cs similarity index 80% rename from BDArmory/Control/AIUtils.cs rename to BDArmory/Utils/AIUtils.cs index 551d3492a..88c8282ea 100644 --- a/BDArmory/Control/AIUtils.cs +++ b/BDArmory/Utils/AIUtils.cs @@ -1,12 +1,14 @@ -using System; -using System.Collections.Generic; -using BDArmory.Core; -using BDArmory.Core.Extension; -using BDArmory.Misc; -using BDArmory.UI; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System; using UnityEngine; -namespace BDArmory.Control +using BDArmory.Control; +using BDArmory.Extensions; +using BDArmory.Settings; +using BDArmory.UI; + +namespace BDArmory.Utils { public static class AIUtils { @@ -16,31 +18,40 @@ public static class AIUtils /// vessel to be extrapolated /// after this time /// Vector3 extrapolated position + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector3 PredictPosition(this Vessel v, float time) { Vector3 pos = v.CoM; pos += v.Velocity() * time; - pos += 0.5f * v.acceleration * time * time; + pos += 0.5f * v.acceleration_immediate * time * time; return pos; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector3 PredictPosition(Vector3 position, Vector3 velocity, Vector3 acceleration, float time) { return position + time * velocity + 0.5f * time * time * acceleration; } + public enum CPAType + { + Earliest, // The earliest future CPA solution. + Latest, // The latest future CPA solution (even if beyond the max time). + Closest // The closest CPA solution within the range 0 — max time. + }; /// /// Predict the next time to the closest point of approach within the next maxTime seconds using the same kinematics as PredictPosition (i.e, position, velocity and acceleration). /// /// The first vessel. /// The second vessel. /// The maximum time to look ahead. + /// When multiple valid solutions exist, return the one of the given type. /// float The time to the closest point of approach within the next maxTime seconds. - public static float ClosestTimeToCPA(this Vessel vessel, Vessel v, float maxTime) - { // Find the closest future time to closest point of approach considering accelerations in addition to velocities. This uses the generalisation of Cardano's solution to finding roots of cubics to find where the derivative of the separation is a minimum. + public static float TimeToCPA(this Vessel vessel, Vessel v, float maxTime = float.MaxValue, CPAType cpaType = CPAType.Earliest) + { // Find the closest/furthest future time to closest point of approach considering accelerations in addition to velocities. This uses the generalisation of Cardano's solution to finding roots of cubics to find where the derivative of the separation is a minimum. if (vessel == null) return 0f; // We don't have a vessel. if (v == null) return 0f; // We don't have a target. - return vessel.ClosestTimeToCPA(v.transform.position, v.Velocity(), v.acceleration, maxTime); + return vessel.TimeToCPA(v.transform.position, v.Velocity(), v.acceleration, maxTime, cpaType); } /// @@ -51,22 +62,33 @@ public static float ClosestTimeToCPA(this Vessel vessel, Vessel v, float maxTime /// The second vessel velocity. /// The second vessel acceleration. /// The maximum time to look ahead. + /// When multiple valid solutions exist, return the one of the given type. /// - public static float ClosestTimeToCPA(this Vessel vessel, Vector3 targetPosition, Vector3 targetVelocity, Vector3 targetAcceleration, float maxTime) + public static float TimeToCPA(this Vessel vessel, Vector3 targetPosition, Vector3 targetVelocity, Vector3 targetAcceleration, float maxTime = float.MaxValue, CPAType cpaType = CPAType.Earliest) { if (vessel == null) return 0f; // We don't have a vessel. Vector3 relPosition = targetPosition - vessel.transform.position; Vector3 relVelocity = targetVelocity - vessel.Velocity(); Vector3 relAcceleration = targetAcceleration - vessel.acceleration; - return ClosestTimeToCPA(relPosition, relVelocity, relAcceleration, maxTime); + return TimeToCPA(relPosition, relVelocity, relAcceleration, maxTime, cpaType); } - public static float ClosestTimeToCPA(Vector3 relPosition, Vector3 relVelocity, Vector3 relAcceleration, float maxTime) + /// + /// Predict the time to the closest point of approach within the next maxTime seconds using the relative position, velocity and acceleration. + /// + /// The relative separation. + /// The relative velocity. + /// The relative acceleration. + /// The maximum time to look ahead. + /// When multiple valid solutions exist, return the one of the given type. + /// + public static float TimeToCPA(Vector3 relPosition, Vector3 relVelocity, Vector3 relAcceleration, float maxTime = float.MaxValue, CPAType cpaType = CPAType.Earliest) { float A = Vector3.Dot(relAcceleration, relAcceleration) / 2f; float B = Vector3.Dot(relVelocity, relAcceleration) * 3f / 2f; float C = Vector3.Dot(relVelocity, relVelocity) + Vector3.Dot(relPosition, relAcceleration); float D = Vector3.Dot(relPosition, relVelocity); + float tolerance = 1e-7f; // Tolerance in comparisons. if (A == 0) // Not actually a cubic. Relative acceleration is zero, so return the much simpler linear timeToCPA. { return Mathf.Clamp(-Vector3.Dot(relPosition, relVelocity) / relVelocity.sqrMagnitude, 0f, maxTime); @@ -75,50 +97,86 @@ public static float ClosestTimeToCPA(Vector3 relPosition, Vector3 relVelocity, V float D1 = 2 * B * B * B - 9f * A * B * C + 27f * A * A * D; float E = D1 * D1 - 4f * D0 * D0 * D0; // = -27*A^2*discriminant // float discriminant = 18f * A * B * C * D - 4f * Mathf.Pow(B, 3f) * D + Mathf.Pow(B, 2f) * Mathf.Pow(C, 2f) - 4f * A * Mathf.Pow(C, 3f) - 27f * Mathf.Pow(A, 2f) * Mathf.Pow(D, 2f); - if (E > 0) + if (E > tolerance) { // Single solution (E is positive) - float F = (D1 + Mathf.Sign(D1) * Mathf.Sqrt(E)) / 2f; + float F = (D1 + Mathf.Sign(D1) * BDAMath.Sqrt(E)) / 2f; float G = Mathf.Sign(F) * Mathf.Pow(Mathf.Abs(F), 1f / 3f); float time = -1f / 3f / A * (B + G + D0 / G); return Mathf.Clamp(time, 0f, maxTime); } - else if (E < 0) + else if (E < -tolerance) { // Triple solution (E is negative) float F_real = D1 / 2f; - float F_imag = Mathf.Sign(D1) * Mathf.Sqrt(-E) / 2f; - float F_abs = Mathf.Sqrt(F_real * F_real + F_imag * F_imag); + float F_imag = Mathf.Sign(D1) * BDAMath.Sqrt(-E) / 2f; + float F_abs = BDAMath.Sqrt(F_real * F_real + F_imag * F_imag); float F_ang = Mathf.Atan2(F_imag, F_real); float G_abs = Mathf.Pow(F_abs, 1f / 3f); float G_ang = F_ang / 3f; float time = -1f; + float distanceSqr = float.MaxValue; for (int i = 0; i < 3; ++i) { float G = G_abs * Mathf.Cos(G_ang + 2f * (float)i * Mathf.PI / 3f); float t = -1f / 3f / A * (B + G + D0 * G / G_abs / G_abs); - if (t > 0f && Mathf.Sign(Vector3.Dot(relVelocity, relVelocity) + Vector3.Dot(relPosition, relAcceleration) + 3f * t * Vector3.Dot(relVelocity, relAcceleration) + 3f / 2f * t * t * Vector3.Dot(relAcceleration, relAcceleration)) > 0) - { // It's a minimum and in the future. - if (time < 0f || t < time) // Update the closest time. - time = t; + if (Mathf.Sign(C + t * B / 2f + 3f * t * t * A) > 0) // It's a minimum. There can be at most 2 minima and 1 maxima. + { + switch (cpaType) + { + case CPAType.Earliest: + if (t > 0 && (time < 0 || t < time)) time = t; + break; + case CPAType.Latest: + if (t > time) time = t; + break; + case CPAType.Closest: + t = Mathf.Clamp(t, 0, maxTime); + var distSqr = (relPosition + relVelocity * t + relAcceleration * t * t / 2f).sqrMagnitude; + if (distSqr < distanceSqr) + { + distanceSqr = distSqr; + time = t; + } + break; + } } } return Mathf.Clamp(time, 0f, maxTime); } else { // Repeated root - if (Mathf.Abs(B * B - 2f * A * C) < 1e-7) + if (Mathf.Abs(B * B - 2f * A * C) < tolerance) { // A triple-root. return Mathf.Clamp(-B / 3f / A, 0f, maxTime); } else { // Double root and simple root. - return Mathf.Clamp(Mathf.Max((9f * A * D - B * C) / 2 / (B * B - 3f * A * C), (4f * A * B * C - 9f * A * A * D - B * B * B) / A / (B * B - 3f * A * C)), 0f, maxTime); + float time = -1f; + float t0 = (9f * A * D - B * C) / 2f / (B * B - 3f * A * C); + float t1 = (4f * A * B * C - 9f * A * A * D - B * B * B) / A / (B * B - 3f * A * C); + switch (cpaType) + { + case CPAType.Earliest: + if (t0 > 0 && (time < 0 || t0 < time)) time = t0; + if (t1 > 0 && (time < 0 || t1 < time)) time = t1; + break; + case CPAType.Latest: + if (t0 > time) time = t0; + if (t1 > time) time = t1; + break; + case CPAType.Closest: + t0 = Mathf.Clamp(t0, 0, maxTime); + t1 = Mathf.Clamp(t1, 0, maxTime); + time = ((relPosition + relVelocity * t0 + relAcceleration * t0 * t0 / 2f).sqrMagnitude < (relPosition + relVelocity * t1 + relAcceleration * t1 * t1 / 2f).sqrMagnitude) ? t0 : t1; + break; + } + return Mathf.Clamp(time, 0, maxTime); } } } public static float PredictClosestApproachSqrSeparation(this Vessel vessel, Vessel otherVessel, float maxTime) { - var timeToCPA = vessel.ClosestTimeToCPA(otherVessel, maxTime); + var timeToCPA = vessel.TimeToCPA(otherVessel, maxTime); if (timeToCPA > 0 && timeToCPA < maxTime) return (vessel.PredictPosition(timeToCPA) - otherVessel.PredictPosition(timeToCPA)).sqrMagnitude; else @@ -159,6 +217,34 @@ public static Vector3 GetLocalFormationPosition(this IBDAIControl ai, int index) return new Vector3(right, back, 0); } + public static Vessel VesselClosestTo(Vector3 position, bool useGeoCoords = false) + { + Vessel closestV = null; + float closestSqrDist = float.MaxValue; + if (FlightGlobals.Vessels == null) return null; + if (useGeoCoords) + { + if (FlightGlobals.currentMainBody is null) return null; + position = (Vector3)FlightGlobals.currentMainBody.GetWorldSurfacePosition(position.x, position.y, position.z); + } + using (var v = FlightGlobals.Vessels.GetEnumerator()) + while (v.MoveNext()) + { + if (v.Current == null || !v.Current.loaded || v.Current.packed) continue; + if (VesselModuleRegistry.ignoredVesselTypes.Contains(v.Current.vesselType)) continue; + var wms = VesselModuleRegistry.GetMissileFire(v.Current); + if (wms != null) + { + if (Vector3.SqrMagnitude(v.Current.vesselTransform.position - position) < closestSqrDist) + { + closestSqrDist = Vector3.SqrMagnitude(v.Current.vesselTransform.position - position); + closestV = v.Current; + } + } + } + return closestV; + } + [Flags] public enum VehicleMovementType { @@ -360,7 +446,7 @@ private void checkGrid(Vector3 origin, CelestialBody body, VehicleMovementType v this.body != body || movementType != vehicleType || this.maxSlopeAngle != maxSlopeAngle * Mathf.Deg2Rad) { GridSize = gridSize; - GridDiagonal = gridSize * Mathf.Sqrt(2); + GridDiagonal = gridSize * BDAMath.Sqrt(2); this.body = body; this.maxSlopeAngle = maxSlopeAngle * Mathf.Deg2Rad; rebuildDistance = Mathf.Clamp(Mathf.Asin(MaxDistortion) * (float)body.Radius, GridSize * 4, GridSize * 256); @@ -474,7 +560,7 @@ private float gridDistance(Coords point, Coords other) Vector3 gridToGeo(float x, float y) { if (x == 0 && y == 0) return origin; - return VectorUtils.GeoCoordinateOffset(origin, body, Mathf.Atan2(y, x) * Mathf.Rad2Deg, Mathf.Sqrt(x * x + y * y) * GridSize); + return VectorUtils.GeoCoordinateOffset(origin, body, Mathf.Atan2(y, x) * Mathf.Rad2Deg, BDAMath.Sqrt(x * x + y * y) * GridSize); } Vector3 gridToGeo(Coords coords) => gridToGeo(coords.X, coords.Y); @@ -650,7 +736,7 @@ public void DrawDebug(Vector3 currentWorldPos, List waypoints = null) using (var kvp = grid.GetEnumerator()) while (kvp.MoveNext()) { - BDGUIUtils.DrawLineBetweenWorldPositions(kvp.Current.Value.WorldPos, kvp.Current.Value.WorldPos + upVec, 3, + GUIUtils.DrawLineBetweenWorldPositions(kvp.Current.Value.WorldPos, kvp.Current.Value.WorldPos + upVec, 3, kvp.Current.Value.Traversable ? Color.green : Color.red); } if (waypoints != null) @@ -660,7 +746,7 @@ public void DrawDebug(Vector3 currentWorldPos, List waypoints = null) while (wp.MoveNext()) { var c = VectorUtils.GetWorldSurfacePostion(wp.Current, body); - BDGUIUtils.DrawLineBetweenWorldPositions(previous + upVec, c + upVec, 2, Color.cyan); + GUIUtils.DrawLineBetweenWorldPositions(previous + upVec, c + upVec, 2, Color.cyan); previous = c; } } diff --git a/BDArmory/Misc/BDAEditorTools.cs b/BDArmory/Utils/BDAEditorTools.cs similarity index 95% rename from BDArmory/Misc/BDAEditorTools.cs rename to BDArmory/Utils/BDAEditorTools.cs index a69c9333c..98ed880cb 100644 --- a/BDArmory/Misc/BDAEditorTools.cs +++ b/BDArmory/Utils/BDAEditorTools.cs @@ -1,10 +1,14 @@ using System; using System.Collections.Generic; -using BDArmory.Core; -using BDArmory.Modules; using UnityEngine; -namespace BDArmory.Misc +using BDArmory.CounterMeasure; +using BDArmory.Radar; +using BDArmory.Settings; +using BDArmory.Weapons; +using BDArmory.Weapons.Missiles; + +namespace BDArmory.Utils { [KSPAddon(KSPAddon.Startup.MainMenu, true)] public class BDAEditorTools : MonoBehaviour @@ -37,10 +41,15 @@ void Awake() GameEvents.onGUIEditorToolbarReady.Add(CheckDump); } + void OnDestroy() + { + GameEvents.onGUIEditorToolbarReady.Remove(CheckDump); + } + void CheckDump() { // dump parts to .CSV list - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_OTHER) dumpParts(); } @@ -58,7 +67,7 @@ public static List getRadars() return results; } - void dumpParts() + public static void dumpParts() { String gunName = "bda_weapons_list.csv"; String missileName = "bda_missile_list.csv"; @@ -86,7 +95,7 @@ void dumpParts() "LASER_BEAMCORRECTIONFACTOR; LASER_BEAMCORRECTIONDAMPING" ); fileradars.WriteLine("NAME;TITLE;AUTHOR;MANUFACTURER;PART_MASS;PART_COST;PART_CRASHTOLERANCE;PART_MAXTEMP;radar_name;rwrThreatType;omnidirectional;directionalFieldOfView;boresightFOV;" + - "scanRotationSpeed;lockRotationSpeed;lockRotationAngle;showDirectionWhileScan;multiLockFOV;lockAttemptFOV;canScan;canLock;canTrackWhileScan;canRecieveRadarData;" + + "scanRotationSpeed;lockRotationSpeed;lockRotationAngle;showDirectionWhileScan;multiLockFOV;lockAttemptFOV;canScan;canLock;canTrackWhileScan;canReceiveRadarData;" + "maxLocks;radarGroundClutterFactor;radarDetectionCurve;radarLockTrackCurve" ); filejammers.WriteLine("NAME;TITLE;AUTHOR;MANUFACTURER;PART_MASS;PART_COST;PART_CRASHTOLERANCE;PART_MAXTEMP;alwaysOn;rcsReduction;rcsReducationFactor;lockbreaker;lockbreak_strength;jammerStrength"); @@ -133,7 +142,7 @@ void dumpParts() item.name + ";" + item.title + ";" + item.author + ";" + item.manufacturer + ";" + item.partPrefab.mass + ";" + item.cost + ";" + item.partPrefab.crashTolerance + ";" + item.partPrefab.maxTemp + ";" + radar.radarName + ";" + radar.getRWRType(radar.rwrThreatType) + ";" + radar.omnidirectional + ";" + radar.directionalFieldOfView + ";" + radar.boresightFOV + ";" + radar.scanRotationSpeed + ";" + radar.lockRotationSpeed + ";" + radar.lockRotationAngle + ";" + radar.showDirectionWhileScan + ";" + radar.multiLockFOV + ";" + radar.lockAttemptFOV + ";" + - radar.canScan + ";" + radar.canLock + ";" + radar.canTrackWhileScan + ";" + radar.canRecieveRadarData + ";" + + radar.canScan + ";" + radar.canLock + ";" + radar.canTrackWhileScan + ";" + radar.canReceiveRadarData + ";" + radar.maxLocks + ";" + radar.radarGroundClutterFactor + ";" + radar.radarDetectionCurve.Evaluate(radar.radarMaxDistanceDetect) + "@" + radar.radarMaxDistanceDetect + ";" + radar.radarLockTrackCurve.Evaluate(radar.radarMaxDistanceLockTrack) + "@" + radar.radarMaxDistanceLockTrack diff --git a/BDArmory/Utils/BDAMath.cs b/BDArmory/Utils/BDAMath.cs new file mode 100644 index 000000000..b1b1a5894 --- /dev/null +++ b/BDArmory/Utils/BDAMath.cs @@ -0,0 +1,73 @@ +using UnityEngine; + +namespace BDArmory.Utils +{ + public static class BDAMath + { + public static float RangedProbability(float[] probs) + { + float total = 0; + foreach (float elem in probs) + { + total += elem; + } + + float randomPoint = UnityEngine.Random.value * total; + + for (int i = 0; i < probs.Length; i++) + { + if (randomPoint < probs[i]) + { + return i; + } + else + { + randomPoint -= probs[i]; + } + } + return probs.Length - 1; + } + + public static bool Between(this float num, float lower, float upper, bool inclusive = true) + { + return inclusive + ? lower <= num && num <= upper + : lower < num && num < upper; + } + + public static Vector3 ProjectOnPlane(Vector3 point, Vector3 planePoint, Vector3 planeNormal) + { + planeNormal = planeNormal.normalized; + + Plane plane = new Plane(planeNormal, planePoint); + float distance = plane.GetDistanceToPoint(point); + + return point - (distance * planeNormal); + } + + public static float SignedAngle(Vector3 fromDirection, Vector3 toDirection, Vector3 referenceRight) + { + float angle = Vector3.Angle(fromDirection, toDirection); + float sign = Mathf.Sign(Vector3.Dot(toDirection, referenceRight)); + float finalAngle = sign * angle; + return finalAngle; + } + + public static float RoundToUnit(float value, float unit = 1f) + { + var rounded = Mathf.Round(value / unit) * unit; + return (unit % 1 != 0) ? rounded : Mathf.Round(rounded); // Fix near-integer loss of precision. + } + + // This is a fun workaround for M1-chip Macs (Apple Silicon). Specific issue the workaround is for is here: + // https://issuetracker.unity3d.com/issues/m1-incorrect-calculation-of-values-using-multiplication-with-mathf-dot-sqrt-when-an-unused-variable-is-declared + public static float Sqrt(float value) => (UI.BDArmorySetup.AppleSilicon) ? SqrtARM(value) : (float)System.Math.Sqrt((double)value); + + private static float SqrtARM(float value) + { + float sqrt = (float)System.Math.Sqrt((double)value); + float sqrt1 = 1f * sqrt; + return sqrt1; + } + } +} diff --git a/BDArmory/Misc/BDAModuleInfos.cs b/BDArmory/Utils/BDAModuleInfos.cs similarity index 89% rename from BDArmory/Misc/BDAModuleInfos.cs rename to BDArmory/Utils/BDAModuleInfos.cs index bce6bf80d..d035c16e1 100644 --- a/BDArmory/Misc/BDAModuleInfos.cs +++ b/BDArmory/Utils/BDAModuleInfos.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using UnityEngine; -namespace BDArmory.Misc +namespace BDArmory.Utils { /// /// This class supports reloading the partModule info blocks when the editor loads. This allows us to obtain current data on the module configurations. @@ -25,9 +25,7 @@ public void Start() internal static IEnumerator ReloadModuleInfos() { - while (Bullets.BulletInfo.bullets == null || Bullets.RocketInfo.rockets == null) // Wait for the field to be non-null to avoid crashes on startup in ModuleWeapon.GetInfo(). - yield return null; - yield return null; + yield return new WaitWhile(() => Bullets.BulletInfo.bullets == null || Bullets.RocketInfo.rockets == null); // Wait for the field to be non-null to avoid crashes on startup in ModuleWeapon.GetInfo(). IEnumerator loadedParts = PartLoader.LoadedPartsList.GetEnumerator(); while (loadedParts.MoveNext()) diff --git a/BDArmory/Misc/BDAcTools.cs b/BDArmory/Utils/BDAcTools.cs similarity index 99% rename from BDArmory/Misc/BDAcTools.cs rename to BDArmory/Utils/BDAcTools.cs index c79e7b269..0f5a23555 100644 --- a/BDArmory/Misc/BDAcTools.cs +++ b/BDArmory/Utils/BDAcTools.cs @@ -4,7 +4,7 @@ using System.Reflection; using UnityEngine; -namespace BDArmory.Misc +namespace BDArmory.Utils { public static class BDAcTools { diff --git a/BDArmory/UI/BDInputInfo.cs b/BDArmory/Utils/BDInputInfo.cs similarity index 94% rename from BDArmory/UI/BDInputInfo.cs rename to BDArmory/Utils/BDInputInfo.cs index f864ec432..78ff26d97 100644 --- a/BDArmory/UI/BDInputInfo.cs +++ b/BDArmory/Utils/BDInputInfo.cs @@ -1,4 +1,4 @@ -namespace BDArmory.UI +namespace BDArmory.Utils { public struct BDInputInfo { diff --git a/BDArmory/UI/BDInputUtils.cs b/BDArmory/Utils/BDInputUtils.cs similarity index 91% rename from BDArmory/UI/BDInputUtils.cs rename to BDArmory/Utils/BDInputUtils.cs index 3b2223da6..418d18446 100644 --- a/BDArmory/UI/BDInputUtils.cs +++ b/BDArmory/Utils/BDInputUtils.cs @@ -2,7 +2,9 @@ using System; using System.Collections; -namespace BDArmory.UI +using BDArmory.Settings; + +namespace BDArmory.Utils { public class BDInputUtils { @@ -15,6 +17,7 @@ public static string GetInputString() for (int i = 0; i < numberOfKeycodes; i++) { string output = names[i]; + if (output.ToLower().StartsWith("mouse") || output.ToLower().StartsWith("joystick")) continue; // Handle mouse and joystick separately. if (output.Contains("Keypad")) { @@ -107,7 +110,8 @@ public static string GetInputString() } catch (System.Exception e) { - Debug.LogWarning("[BDArmory.BDInputUtils]: Exception thrown in GetInputString: " + e.Message + "\n" + e.StackTrace); + if (!e.Message.EndsWith("is unknown")) // Ignore unknown keys + Debug.LogWarning("[BDArmory.BDInputUtils]: Exception thrown in GetInputString: " + e.Message + "\n" + e.StackTrace); } } @@ -155,12 +159,12 @@ public static string GetInputString() public static bool GetKey(BDInputInfo input) { - return input.inputString != string.Empty && Input.GetKey(input.inputString); + return !string.IsNullOrEmpty(input.inputString) && Input.GetKey(input.inputString); } public static bool GetKeyDown(BDInputInfo input) { - return input.inputString != string.Empty && Input.GetKeyDown(input.inputString); + return !string.IsNullOrEmpty(input.inputString) && Input.GetKeyDown(input.inputString); } } @@ -176,7 +180,7 @@ public class NumericInputField : MonoBehaviour public string possibleValue = string.Empty; private double _value; public double currentValue { get { return _value; } set { _value = value; possibleValue = _value.ToString("G6"); } } - private double minValue; + public double minValue; public double maxValue; private bool coroutineRunning = false; private Coroutine coroutine; diff --git a/BDArmory/UI/BDKeyBinder.cs b/BDArmory/Utils/BDKeyBinder.cs similarity index 98% rename from BDArmory/UI/BDKeyBinder.cs rename to BDArmory/Utils/BDKeyBinder.cs index 15ac24b1c..64676f8a8 100644 --- a/BDArmory/UI/BDKeyBinder.cs +++ b/BDArmory/Utils/BDKeyBinder.cs @@ -1,7 +1,7 @@ using System.Collections; using UnityEngine; -namespace BDArmory.UI +namespace BDArmory.Utils { public class BDKeyBinder : MonoBehaviour { diff --git a/BDArmory.Core/Utils/BlastPhysicsUtils.cs b/BDArmory/Utils/BlastPhysicsUtils.cs similarity index 75% rename from BDArmory.Core/Utils/BlastPhysicsUtils.cs rename to BDArmory/Utils/BlastPhysicsUtils.cs index 605c7acf8..a69709787 100644 --- a/BDArmory.Core/Utils/BlastPhysicsUtils.cs +++ b/BDArmory/Utils/BlastPhysicsUtils.cs @@ -1,8 +1,10 @@ using System; -using BDArmory.Core.Extension; using UnityEngine; -namespace BDArmory.Core.Utils +using BDArmory.Extensions; +using BDArmory.Settings; + +namespace BDArmory.Utils { public static class BlastPhysicsUtils { @@ -48,13 +50,14 @@ public static BlastInfo CalculatePartBlastEffects(Part part, float distanceToHit float finalDamage = (float)totalDamage; - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_DAMAGE) { Debug.Log( "[BDArmory.BlastPhysicsUtils]: Blast Debug data: {" + part.name + "}, " + " clampedMinDistanceToHit: {" + clampedMinDistanceToHit + "}," + " minPressureDistance: {" + minPressureDistance + "}," + " minScaledDistance: {" + minScaledDistance + "}," + + " maxScaledDistance: {" + maxScaledDistance + "}," + " minPressurePerMs: {" + minPressurePerMs + "}," + " maxPressurePerMs: {" + maxPressurePerMs + "}," + " minDistPositivePhase: {" + minDistPositivePhase + "}," + @@ -93,21 +96,30 @@ private static double CalculateIncidentImpulse(double scaledDistance, float expl if (scaledDistance <= 0.955) { //NATO version double U = 2.06761908721 + 3.0760329666 * t; + var U2 = U * U; + var U3 = U2 * U; + var U4 = U3 * U; ii = 2.52455620925 - 0.502992763686 * U + - 0.171335645235 * Math.Pow(U, 2) + - 0.0450176963051 * Math.Pow(U, 3) - - 0.0118964626402 * Math.Pow(U, 4); + 0.171335645235 * U2 + + 0.0450176963051 * U3 - + 0.0118964626402 * U4; } else if (scaledDistance > 0.955) { //version from ??? var U = -1.94708846747 + 2.40697745406 * t; + var U2 = U * U; + var U3 = U2 * U; + var U4 = U3 * U; + var U5 = U4 * U; + var U6 = U5 * U; + var U7 = U6 * U; ii = 1.67281645863 - 0.384519026965 * U - - 0.0260816706301 * Math.Pow(U, 2) + - 0.00595798753822 * Math.Pow(U, 3) + - 0.014544526107 * Math.Pow(U, 4) - - 0.00663289334734 * Math.Pow(U, 5) - - 0.00284189327204 * Math.Pow(U, 6) + - 0.0013644816227 * Math.Pow(U, 7); + 0.0260816706301 * U2 + + 0.00595798753822 * U3 + + 0.014544526107 * U4 - + 0.00663289334734 * U5 - + 0.00284189327204 * U6 + + 0.0013644816227 * U7; } ii = Math.Pow(10, ii); @@ -126,32 +138,47 @@ private static double CalculatePositivePhaseTime(double scaledDistance, float ex if (scaledDistance <= 1.01) { double U = 1.92946154068 + 5.25099193925 * t; + var U2 = U * U; + var U3 = U2 * U; + var U4 = U3 * U; + var U5 = U4 * U; ii = -0.614227603559 + 0.130143717675 * U + - 0.134872511954 * Math.Pow(U, 2) + - 0.0391574276906 * Math.Pow(U, 3) - - 0.00475933664702 * Math.Pow(U, 4) - - 0.00428144598008 * Math.Pow(U, 5); + 0.134872511954 * U2 + + 0.0391574276906 * U3 - + 0.00475933664702 * U4 - + 0.00428144598008 * U5; } else if (scaledDistance <= 2.78) { double U = -2.12492525216 + 9.2996288611 * t; + var U2 = U * U; + var U3 = U2 * U; + var U4 = U3 * U; + var U5 = U4 * U; + var U6 = U5 * U; + var U7 = U6 * U; + var U8 = U7 * U; ii = 0.315409245784 - 0.0297944268976 * U + - 0.030632954288 * Math.Pow(U, 2) + - 0.0183405574086 * Math.Pow(U, 3) - - 0.0173964666211 * Math.Pow(U, 4) - - 0.00106321963633 * Math.Pow(U, 5) + - 0.00562060030977 * Math.Pow(U, 6) + - 0.0001618217499 * Math.Pow(U, 7) - - 0.0006860188944 * Math.Pow(U, 8); + 0.030632954288 * U2 + + 0.0183405574086 * U3 - + 0.0173964666211 * U4 - + 0.00106321963633 * U5 + + 0.00562060030977 * U6 + + 0.0001618217499 * U7 - + 0.0006860188944 * U8; } else // scaledDistance > 2.78 { double U = -3.53626218091 + 3.46349745571 * t; + var U2 = U * U; + var U3 = U2 * U; + var U4 = U3 * U; + var U5 = U4 * U; ii = 0.686906642409 + 0.0933035304009 * U - - 0.0005849420883 * Math.Pow(U, 2) - - 0.00226884995013 * Math.Pow(U, 3) - - 0.00295908591505 * Math.Pow(U, 4) + - 0.00148029868929 * Math.Pow(U, 5); + 0.0005849420883 * U2 - + 0.00226884995013 * U3 - + 0.00295908591505 * U4 + + 0.00148029868929 * U5; } ii = Math.Pow(10, ii); @@ -171,15 +198,23 @@ public static float CalculateMaxTime(float tntMass) double ii = 0; double U = -0.202425716178 + 1.37784223635 * t; + var U2 = U * U; + var U3 = U2 * U; + var U4 = U3 * U; + var U5 = U4 * U; + var U6 = U5 * U; + var U7 = U6 * U; + var U8 = U7 * U; + var U9 = U8 * U; ii = -0.0591634288046 + 1.35706496258 * U + - 0.052492798645 * Math.Pow(U, 2) - - 0.196563954086 * Math.Pow(U, 3) - - 0.0601770052288 * Math.Pow(U, 4) + - 0.0696360270981 * Math.Pow(U, 5) + - 0.0215297490092 * Math.Pow(U, 6) - - 0.0161658930785 * Math.Pow(U, 7) - - 0.00232531970294 * Math.Pow(U, 8) + - 0.00147752067524 * Math.Pow(U, 9); + 0.052492798645 * U2 - + 0.196563954086 * U3 - + 0.0601770052288 * U4 + + 0.0696360270981 * U5 + + 0.0215297490092 * U6 - + 0.0161658930785 * U7 - + 0.00232531970294 * U8 + + 0.00147752067524 * U9; ii = Math.Pow(10, ii); ii = ii * cubeRootOfChargeWeight / 1000f; @@ -216,7 +251,8 @@ public static float CalculateBlastRange(double tntMass) /// explosive range in meters public static float CalculateExplosiveMass(float range) { - return (float)Math.Pow((range / 14.8f), 3); + var scaledRange = range / 14.8f; + return (float)(scaledRange * scaledRange * scaledRange); } } diff --git a/BDArmory/Utils/BodyUtils.cs b/BDArmory/Utils/BodyUtils.cs new file mode 100644 index 000000000..64b9a82a8 --- /dev/null +++ b/BDArmory/Utils/BodyUtils.cs @@ -0,0 +1,100 @@ +using System; +using UnityEngine; + +namespace BDArmory.Utils +{ + public static class BodyUtils + { + public static string FormattedGeoPos(Vector3d geoPos, bool altitude) + { + string finalString = string.Empty; + //lat + double lat = geoPos.x; + double latSign = Math.Sign(lat); + double latMajor = latSign * Math.Floor(Math.Abs(lat)); + double latMinor = 100 * (Math.Abs(lat) - Math.Abs(latMajor)); + string latString = latMajor.ToString("0") + " " + latMinor.ToString("0.000"); + finalString += "N:" + latString; + + //longi + double longi = geoPos.y; + double longiSign = Math.Sign(longi); + double longiMajor = longiSign * Math.Floor(Math.Abs(longi)); + double longiMinor = 100 * (Math.Abs(longi) - Math.Abs(longiMajor)); + string longiString = longiMajor.ToString("0") + " " + longiMinor.ToString("0.000"); + finalString += " E:" + longiString; + + if (altitude) + { + finalString += " ASL:" + geoPos.z.ToString("0.000"); + } + + return finalString; + } + + public static string FormattedGeoPosShort(Vector3d geoPos, bool altitude) + { + string finalString = string.Empty; + //lat + double lat = geoPos.x; + double latSign = Math.Sign(lat); + double latMajor = latSign * Math.Floor(Math.Abs(lat)); + double latMinor = 100 * (Math.Abs(lat) - Math.Abs(latMajor)); + string latString = latMajor.ToString("0") + " " + latMinor.ToString("0"); + finalString += "N:" + latString; + + //longi + double longi = geoPos.y; + double longiSign = Math.Sign(longi); + double longiMajor = longiSign * Math.Floor(Math.Abs(longi)); + double longiMinor = 100 * (Math.Abs(longi) - Math.Abs(longiMajor)); + string longiString = longiMajor.ToString("0") + " " + longiMinor.ToString("0"); + finalString += " E:" + longiString; + + if (altitude) + { + finalString += " ASL:" + geoPos.z.ToString("0"); + } + + return finalString; + } + + public static float GetRadarAltitudeAtPos(Vector3 position, bool clamped = true) + { + double latitudeAtPos = FlightGlobals.currentMainBody.GetLatitude(position); + double longitudeAtPos = FlightGlobals.currentMainBody.GetLongitude(position); + float altitude = (float)(FlightGlobals.currentMainBody.GetAltitude(position)); + if (clamped) + return Mathf.Clamp(altitude - (float)FlightGlobals.currentMainBody.TerrainAltitude(latitudeAtPos, longitudeAtPos), 0, altitude); + else + return altitude - (float)FlightGlobals.currentMainBody.TerrainAltitude(latitudeAtPos, longitudeAtPos); + } + + public static double GetTerrainAltitudeAtPos(Vector3 position, bool allowNegative = false) + { + double latitudeAtPos = FlightGlobals.currentMainBody.GetLatitude(position); + double longitudeAtPos = FlightGlobals.currentMainBody.GetLongitude(position); + return FlightGlobals.currentMainBody.TerrainAltitude(latitudeAtPos, longitudeAtPos, allowNegative); + } + + /// + /// Get the surface normal directly below the position. + /// Note: this uses a raycast, so may simply return vertical far away where terrain colliders aren't loaded. + /// + /// The position below which to get the surface normal. + /// Include terrain below ocean level (true) or not (false). + /// + public static Vector3d GetSurfaceNormal(Vector3 position, bool allowNegative = false) + { + var latitudeAtPos = FlightGlobals.currentMainBody.GetLatitude(position); + var longitudeAtPos = FlightGlobals.currentMainBody.GetLongitude(position); + var radial = new Ray(position, position - FlightGlobals.currentMainBody.transform.position); + var altitude = FlightGlobals.currentMainBody.GetAltitude(position); + if (!allowNegative && altitude <= 0) return radial.direction; // Ocean surface. + var terrainAltitude = FlightGlobals.currentMainBody.TerrainAltitude(latitudeAtPos, longitudeAtPos); + if (Physics.Raycast(radial.GetPoint(1f + (float)(terrainAltitude - altitude)), -radial.direction, out RaycastHit hit, 2f, (int)LayerMasks.Scenery)) + return hit.normal; + return radial.direction; + } + } +} \ No newline at end of file diff --git a/BDArmory.Core/Utils/BulletPhysics.cs b/BDArmory/Utils/BulletPhysics.cs similarity index 91% rename from BDArmory.Core/Utils/BulletPhysics.cs rename to BDArmory/Utils/BulletPhysics.cs index f8e7f3ed8..9098f0c49 100644 --- a/BDArmory.Core/Utils/BulletPhysics.cs +++ b/BDArmory/Utils/BulletPhysics.cs @@ -1,6 +1,6 @@ using UnityEngine; -namespace BDArmory.Core.Utils +namespace BDArmory.Utils { public class BulletPhysics : MonoBehaviour { @@ -24,7 +24,7 @@ public static Vector3 CalculateDrag(Vector3 velocity, float bulletMass, float ca //Vector3 dragVec = aDrag * velocityVec.normalized * -1f; /////////////////////////////////////////////////////// - float bulletDragArea = Mathf.PI * Mathf.Pow(caliber / 2f, 2f); + float bulletDragArea = Mathf.PI * caliber * caliber / 4f; float bulletBallisticCoefficient = ((bulletMass * 1000) / (bulletDragArea * 0.295f)); float k = 0.5f * bulletBallisticCoefficient * rho * bulletDragArea; diff --git a/BDArmory.Core/Utils/ConfigNodeUtils.cs b/BDArmory/Utils/ConfigNodeUtils.cs similarity index 98% rename from BDArmory.Core/Utils/ConfigNodeUtils.cs rename to BDArmory/Utils/ConfigNodeUtils.cs index a5cbb9adb..457200aa0 100644 --- a/BDArmory.Core/Utils/ConfigNodeUtils.cs +++ b/BDArmory/Utils/ConfigNodeUtils.cs @@ -1,6 +1,6 @@ using UnityEngine; -namespace BDArmory.Core.Utils +namespace BDArmory.Utils { public class ConfigNodeUtils { diff --git a/BDArmory.Core/Utils/DebugUtils.cs b/BDArmory/Utils/DebugUtils.cs similarity index 93% rename from BDArmory.Core/Utils/DebugUtils.cs rename to BDArmory/Utils/DebugUtils.cs index 9c051be57..da719a84e 100644 --- a/BDArmory.Core/Utils/DebugUtils.cs +++ b/BDArmory/Utils/DebugUtils.cs @@ -1,4 +1,4 @@ -namespace BDArmory.Core.Utils +namespace BDArmory.Utils { internal class DebugUtils { diff --git a/BDArmory/Misc/DecoupledBooster.cs b/BDArmory/Utils/DecoupledBooster.cs similarity index 92% rename from BDArmory/Misc/DecoupledBooster.cs rename to BDArmory/Utils/DecoupledBooster.cs index 653b98219..99d2a65ef 100644 --- a/BDArmory/Misc/DecoupledBooster.cs +++ b/BDArmory/Utils/DecoupledBooster.cs @@ -3,7 +3,7 @@ using UniLinq; using UnityEngine; -namespace BDArmory.Misc +namespace BDArmory.Utils { public class DecoupledBooster : MonoBehaviour { @@ -18,7 +18,7 @@ IEnumerator SelfDestructRoutine() col.Current.enabled = false; } col.Dispose(); - yield return new WaitForSeconds(5); + yield return new WaitForSecondsFixed(5); Destroy(gameObject); } diff --git a/BDArmory/Utils/FARUtils.cs b/BDArmory/Utils/FARUtils.cs new file mode 100644 index 000000000..100972c35 --- /dev/null +++ b/BDArmory/Utils/FARUtils.cs @@ -0,0 +1,298 @@ +using System; +using System.Reflection; +using UnityEngine; + +using BDArmory.Settings; + +namespace BDArmory.Utils +{ + [KSPAddon(KSPAddon.Startup.MainMenu, true)] + public class FerramAerospace : MonoBehaviour + { + public static FerramAerospace Instance; + public static bool hasFAR = false; + private static bool hasCheckedForFAR = false; + public static bool hasFARWing = false; + public static bool hasFARControllableSurface = false; + private static bool hasCheckedForFARWing = false; + private static bool hasCheckedForFARControllableSurface = false; + + public static Assembly FARAssembly; + public static Type FARWingModule; + public static Type FARControllableSurfaceModule; + + + void Awake() + { + if (Instance != null) return; // Don't replace existing instance. + Instance = new FerramAerospace(); + } + + void Start() + { + CheckForFAR(); + if (hasFAR) + { + CheckForFARWing(); + CheckForFARControllableSurface(); + } + } + + public static bool CheckForFAR() + { + if (hasCheckedForFAR) return hasFAR; + hasCheckedForFAR = true; + foreach (var assy in AssemblyLoader.loadedAssemblies) + { + if (assy.assembly.FullName.StartsWith("FerramAerospaceResearch")) + { + FARAssembly = assy.assembly; + hasFAR = true; + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.FARUtils]: Found FAR Assembly: {FARAssembly.FullName}"); + } + } + return hasFAR; + } + + public static bool CheckForFARWing() + { + if (!hasFAR) return false; + if (hasCheckedForFARWing) return hasFARWing; + hasCheckedForFARWing = true; + foreach (var type in FARAssembly.GetTypes()) + { + if (type.Name == "FARWingAerodynamicModel") + { + FARWingModule = type; + hasFARWing = true; + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.FARUtils]: Found FAR wing module type."); + } + } + return hasFARWing; + } + + public static bool CheckForFARControllableSurface() + { + if (!hasFAR) return false; + if (hasCheckedForFARControllableSurface) return hasFARControllableSurface; + hasCheckedForFARControllableSurface = true; + foreach (var type in FARAssembly.GetTypes()) + { + if (type.Name == "FARControllableSurface") + { + FARControllableSurfaceModule = type; + hasFARControllableSurface = true; + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.FARUtils]: Found FAR controllable surface module type."); + } + } + return hasFARControllableSurface; + } + + public static float GetFARMassMult(Part part) + { + if (!hasFARWing) return 1; + + foreach (var module in part.Modules) + { + if (module.GetType() == FARWingModule) + { + var massMultiplier = (float)FARWingModule.GetField("massMultiplier", BindingFlags.Public | BindingFlags.Instance).GetValue(module); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.FARUtils]: Found wing Mass multiplier of {massMultiplier} for {part.name}."); + return massMultiplier; + } + if (module.GetType() == FARControllableSurfaceModule) + { + var massMultiplier = (float)FARControllableSurfaceModule.GetField("massMultiplier", BindingFlags.Public | BindingFlags.Instance).GetValue(module); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.FARUtils]: Found ctrl. srf. Mass multiplier of {massMultiplier} for {part.name}."); + return massMultiplier; + } + } + return 1; + } + } + + public class ProceduralWing : MonoBehaviour + { + public static ProceduralWing Instance; + public static bool hasB9ProcWing = false; + private static bool hasCheckedForB9PW = false; + public static bool hasPwingModule = false; + private static bool hasCheckedForPwingModule = false; + + public static Assembly PWAssembly; + public static Type PWType; + + + void Awake() + { + if (Instance != null) return; // Don't replace existing instance. + Instance = new ProceduralWing(); + } + + void Start() + { + CheckForB9ProcWing(); + if (hasB9ProcWing) CheckForPWModule(); + } + + public static bool CheckForB9ProcWing() + { + if (hasCheckedForB9PW) return hasB9ProcWing; + hasCheckedForB9PW = true; + foreach (var assy in AssemblyLoader.loadedAssemblies) + { + if (assy.assembly.FullName.Contains("B9") && assy.assembly.FullName.Contains("PWings")) // Not finding 'if (assy.assembly.FullName.StartsWith("B9-PWings-Fork"))'? + { + PWAssembly = assy.assembly; + hasB9ProcWing = true; + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.FARUtils]: Found Pwing Assembly: {PWAssembly.FullName}"); + } + } + return hasB9ProcWing; + } + + public static bool CheckForPWModule() + { + if (!hasB9ProcWing) return false; + if (hasCheckedForPwingModule) return hasPwingModule; + hasCheckedForPwingModule = true; + foreach (var type in PWAssembly.GetTypes()) + { + //Debug.Log($"[BDArmory.FARUtils]: Found module " + type.Name); + + if (type.Name == "WingProcedural") + { + PWType = type; + hasPwingModule = true; + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.FARUtils]: Found Pwing module."); + } + } + return hasPwingModule; + } + + public static float GetPWingVolume(Part part) + { + if (!hasPwingModule) + { + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.FARUtils]: hasPwing check failed!"); + return -1; + } + + foreach (var module in part.Modules) + { + if (module.GetType() == PWType || module.GetType().IsSubclassOf(PWType)) + { + if (module.GetType() == PWType) + { + bool WingctrlSrf = (bool)PWType.GetField("isWingAsCtrlSrf", BindingFlags.Public | BindingFlags.Instance).GetValue(module); + bool ctrlSrf = (bool)PWType.GetField("isCtrlSrf", BindingFlags.Public | BindingFlags.Instance).GetValue(module); + float length = (float)PWType.GetField("sharedBaseLength", BindingFlags.Public | BindingFlags.Instance).GetValue(module); + float width = ((float)PWType.GetField("sharedBaseWidthRoot", BindingFlags.Public | BindingFlags.Instance).GetValue(module) + (float)PWType.GetField("sharedBaseWidthTip", BindingFlags.Public | BindingFlags.Instance).GetValue(module)); + float thickness = 0.36f; + float adjustedThickness = 0.36f; + if (BDArmorySettings.RUNWAY_PROJECT || BDArmorySettings.PWING_THICKNESS_AFFECT_MASS_HP) + //if (BDArmorySettings.PWING_THICKNESS_AFFECT_MASS_HP) + { + thickness = Mathf.Max(((float)PWType.GetField("sharedBaseThicknessRoot", BindingFlags.Public | BindingFlags.Instance).GetValue(module) + (float)PWType.GetField("sharedBaseThicknessTip", BindingFlags.Public | BindingFlags.Instance).GetValue(module)), 0.2f); + if (thickness >= 0.36f) //0.18 * 2 + adjustedThickness = (Mathf.Max(0.36f, (Mathf.Log(1.55f + (thickness * 0.5f)) * 0.66f))); + else + adjustedThickness = Mathf.Max(thickness, 0.2f); + //thickness = 0.36f; //thickness doesn't add to pwing mass, so why should it add to HP? + //- because edge lift doesn't contribute to HP anymore, and past a certain thickness, the increased height of the collider is an issue + //will also incentivise using a single thick wing instead of wing sandwiching + //look into making thickness add mass? + //-that seems like a change that really should be part of pwings proper, not bolted on here, even if it really would help balance out pwings... + } + //float thickness = 0.36f; + float aeroVolume = (0.786f * length * width * adjustedThickness) / 4f; //original .7 was based on errorneous 2x4 wingboard dimensions; stock reference wing area is 1.875x3.75m + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.FARUtils]: Found volume of {aeroVolume} for {part.name}."); + float liftCoeff = (float)PWType.GetField("stockLiftCoefficient", BindingFlags.Public | BindingFlags.Instance).GetValue(module); + if ((BDArmorySettings.RUNWAY_PROJECT || !BDArmorySettings.PWING_EDGE_LIFT) && !ctrlSrf) //if RunwayProject and part !controlsurface, remove lift/mass from edges to bring inline with stock boards + { + bool isLiftingSurface = (float)PWType.GetField("stockLiftCoefficient", BindingFlags.Public | BindingFlags.Instance).GetValue(module) > 0f; + liftCoeff = (length * (width / 2f)) / 3.52f; + if (BDArmorySettings.RUNWAY_PROJECT) liftCoeff = Mathf.Clamp(liftCoeff, 0, BDArmorySettings.MAX_PWING_LIFT); + PWType.GetField("stockLiftCoefficient", BindingFlags.Public | BindingFlags.Instance).SetValue(module, isLiftingSurface ? liftCoeff : 0f); //adjust PWing GUI lift readout + part.Modules.GetModule().deflectionLiftCoeff = (length * (width / 2f) / 3.52f); //adjust lift to be inline with stock wings + } + if (!WingctrlSrf) + PWType.GetField("aeroUIMass", BindingFlags.Public | BindingFlags.Instance).SetValue(module, (liftCoeff / 10f) * (thickness * 3)); //Adjust PWing GUI mass readout + else + PWType.GetField("aeroUIMass", BindingFlags.Public | BindingFlags.Instance).SetValue(module, (liftCoeff / 5f) * (thickness * 3)); //this modifies the IPartMassModifier, so the mass will also change along with the GUI + + if (part.name.Contains("B9.Aero.Wing.Procedural.Panel")) //if Josue's noLift PWings PR never gets folded in, here's an alternative using an MM'ed PWing structural panel part + { + PWType.GetField("stockLiftCoefficient", BindingFlags.Public | BindingFlags.Instance).SetValue(module, 0f); //adjust PWing GUI lift readout + PWType.GetField("aeroUIMass", BindingFlags.Public | BindingFlags.Instance).SetValue(module, (((length * (width / 2f)) / 3.52f) / 12.5f) * (Mathf.Max(0.6f, thickness * 3))); //Struct panels lighter than wings, clamp mass for panels thinner than 0.1m + part.FindModuleImplementing().deflectionLiftCoeff = 0; + //PWType.GetField("aeroUIMass", BindingFlags.Public | BindingFlags.Instance).SetValue(module, (((length * (width / 2f)) / 3.52f) / 12.5f) * (thickness / 0.18f)); //version that has mass based on panel thickness + } + //Pcontrol surfaces are more difficult; can easily be just an edge with span thickness = 0, so removing lift from these would render them essentially decorative and nothing else + //...add a child boxCollider...? Again, a change that really should be done in Pwings, not here + return aeroVolume; + } + } + } + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.FARUtils]: Pwing module not found!"); + return -1; + } + + public static float ResetPWing(Part part) + { + if (!hasPwingModule) + { + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.FARUtils]: hasPwing check failed!"); + return 0; + } + + foreach (var module in part.Modules) + { + if (module.GetType() == PWType || module.GetType().IsSubclassOf(PWType)) + { + if (module.GetType() == PWType) + { + bool ctrlSrf = (bool)PWType.GetField("isCtrlSrf", BindingFlags.Public | BindingFlags.Instance).GetValue(module); + bool WingctrlSrf = (bool)PWType.GetField("isWingAsCtrlSrf", BindingFlags.Public | BindingFlags.Instance).GetValue(module); + + if (ctrlSrf) return 0; //control surfaces don't have any lift modification to begin with + double originalLift = (double)PWType.GetField("aeroStatSurfaceArea", BindingFlags.Public | BindingFlags.Instance).GetValue(module); + originalLift /= 3.52f; + + bool isLiftingSurface = (float)PWType.GetField("stockLiftCoefficient", BindingFlags.Public | BindingFlags.Instance).GetValue(module) > 0f; + + PWType.GetField("stockLiftCoefficient", BindingFlags.Public | BindingFlags.Instance).SetValue(module, isLiftingSurface ? (float)originalLift : 0f); //restore lift value/ correct GUI readout + part.Modules.GetModule().deflectionLiftCoeff = (float)Math.Round((float)originalLift, 2); + if (!WingctrlSrf) + PWType.GetField("aeroUIMass", BindingFlags.Public | BindingFlags.Instance).SetValue(module, (float)originalLift / 10f); + else + PWType.GetField("aeroUIMass", BindingFlags.Public | BindingFlags.Instance).SetValue(module, (float)originalLift / 5f); + } + } + } + + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.FARUtils]: Pwing module not found!"); + return 0; + } + + public static float GetPWingArea(Part part) + { + if (!hasPwingModule) return -1; + + foreach (var module in part.Modules) + { + if (module.GetType() == PWType) + { + var length = (float)PWType.GetField("sharedBaseLength", BindingFlags.Public | BindingFlags.Instance).GetValue(module); + var width = ((float)PWType.GetField("sharedBaseWidthRoot", BindingFlags.Public | BindingFlags.Instance).GetValue(module) + (float)PWType.GetField("sharedBaseWidthTip", BindingFlags.Public | BindingFlags.Instance).GetValue(module)) / 2; + var thickness = ((float)PWType.GetField("sharedBaseThicknessRoot", BindingFlags.Public | BindingFlags.Instance).GetValue(module) + (float)PWType.GetField("sharedBaseThicknessTip", BindingFlags.Public | BindingFlags.Instance).GetValue(module)) / 2; + float area = (2 * (length * width)) + (2 * (width * thickness)) + (2 * (length * thickness)); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.FARUtils]: Found wing area of {area}: {length} * {width} * {thickness} * 2 for {part.name}."); + if (thickness <= 0.25f) area /= 2; //for ~stock thickness wings, halve area to prevent to prevent double armor. Thicker wings/Wings ued as structural elements that can conceivably have other stuff inside them, treat as standard part for armor volume + return area; + } + } + return -1; + } + } +} diff --git a/BDArmory/Misc/FireSpitter.cs b/BDArmory/Utils/FireSpitterUtils.cs similarity index 88% rename from BDArmory/Misc/FireSpitter.cs rename to BDArmory/Utils/FireSpitterUtils.cs index 70d8cdb40..f89fe482f 100644 --- a/BDArmory/Misc/FireSpitter.cs +++ b/BDArmory/Utils/FireSpitterUtils.cs @@ -1,10 +1,10 @@ using System; using System.Reflection; using UnityEngine; -using System.Linq; -using BDArmory.Core; -namespace BDArmory.Misc +using BDArmory.Settings; + +namespace BDArmory.Utils { [KSPAddon(KSPAddon.Startup.MainMenu, true)] public class FireSpitter : MonoBehaviour @@ -41,7 +41,7 @@ public static bool CheckForFireSpitter() { FSAssembly = assy.assembly; hasFireSpitter = true; - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log($"[BDArmory.FireSpitter]: Found FireSpitter Assembly: {FSAssembly.FullName}"); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.FireSpitter]: Found FireSpitter Assembly: {FSAssembly.FullName}"); } } return hasFireSpitter; @@ -58,7 +58,7 @@ public static bool CheckForFSEngine() { FSEngineType = type; hasFSEngine = true; - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log($"[BDArmory.FireSpitter]: Found FSengine type."); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.FireSpitter]: Found FSengine type."); } } return hasFSEngine; @@ -75,12 +75,12 @@ public static void ActivateFSEngines(Vessel vessel, bool activate = true) { if (activate) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log($"[BDArmory.FireSpitter]: Found {module} on {vessel.vesselName}, attempting to call 'Activate'."); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.FireSpitter]: Found {module} on {vessel.vesselName}, attempting to call 'Activate'."); FSEngineType.InvokeMember("Activate", BindingFlags.InvokeMethod, null, module, new object[] { }); // Note: this activates the engines, but the throttle on the engines aren't controlled unless they're on the active vessel. } else { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log($"[BDArmory.FireSpitter]: Found {module} on {vessel.vesselName}, attempting to call 'Shutdown'."); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.FireSpitter]: Found {module} on {vessel.vesselName}, attempting to call 'Shutdown'."); FSEngineType.InvokeMember("Shutdown", BindingFlags.InvokeMethod, null, module, new object[] { }); } } diff --git a/BDArmory/Utils/GUIUtils.cs b/BDArmory/Utils/GUIUtils.cs new file mode 100644 index 000000000..c9c80ae5d --- /dev/null +++ b/BDArmory/Utils/GUIUtils.cs @@ -0,0 +1,508 @@ +using System.Collections.Generic; +using System; +using UniLinq; +using UnityEngine; + +using BDArmory.Control; +using BDArmory.Settings; +using BDArmory.Targeting; +using BDArmory.UI; + +namespace BDArmory.Utils +{ + public static class GUIUtils + { + public static Texture2D pixel; + + public static Camera GetMainCamera() + { + if (HighLogic.LoadedSceneIsFlight) + { + return FlightCamera.fetch.mainCamera; + } + else + { + return Camera.main; + } + } + + public static void DrawTextureOnWorldPos(Vector3 worldPos, Texture texture, Vector2 size, float wobble) + { + var cam = GetMainCamera(); + if (cam == null) return; + Vector3 screenPos = cam.WorldToViewportPoint(worldPos); + if (screenPos.z < 0) return; //dont draw if point is behind camera + if (screenPos.x != Mathf.Clamp01(screenPos.x)) return; //dont draw if off screen + if (screenPos.y != Mathf.Clamp01(screenPos.y)) return; + float xPos = screenPos.x * Screen.width - (0.5f * size.x); + float yPos = (1 - screenPos.y) * Screen.height - (0.5f * size.y); + if (wobble > 0) + { + xPos += UnityEngine.Random.Range(-wobble / 2, wobble / 2); + yPos += UnityEngine.Random.Range(-wobble / 2, wobble / 2); + } + Rect iconRect = new Rect(xPos, yPos, size.x, size.y); + + GUI.DrawTexture(iconRect, texture); + } + + public static bool WorldToGUIPos(Vector3 worldPos, out Vector2 guiPos) + { + var cam = GetMainCamera(); + if (cam == null) + { + guiPos = Vector2.zero; + return false; + } + Vector3 screenPos = cam.WorldToViewportPoint(worldPos); + bool offScreen = false; + if (screenPos.z < 0) offScreen = true; //dont draw if point is behind camera + if (screenPos.x != Mathf.Clamp01(screenPos.x)) offScreen = true; //dont draw if off screen + if (screenPos.y != Mathf.Clamp01(screenPos.y)) offScreen = true; + if (!offScreen) + { + float xPos = screenPos.x * Screen.width; + float yPos = (1 - screenPos.y) * Screen.height; + guiPos = new Vector2(xPos, yPos); + return true; + } + else + { + guiPos = Vector2.zero; + return false; + } + } + + public static void DrawLineBetweenWorldPositions(Vector3 worldPosA, Vector3 worldPosB, float width, Color color) + { + Camera cam = GetMainCamera(); + + if (cam == null) return; + + GUI.matrix = Matrix4x4.identity; + + bool aBehind = false; + + Plane clipPlane = new Plane(cam.transform.forward, cam.transform.position + cam.transform.forward * 0.05f); + + if (Vector3.Dot(cam.transform.forward, worldPosA - cam.transform.position) < 0) + { + Ray ray = new Ray(worldPosB, worldPosA - worldPosB); + float dist; + if (clipPlane.Raycast(ray, out dist)) + { + worldPosA = ray.GetPoint(dist); + } + aBehind = true; + } + if (Vector3.Dot(cam.transform.forward, worldPosB - cam.transform.position) < 0) + { + if (aBehind) return; + + Ray ray = new Ray(worldPosA, worldPosB - worldPosA); + float dist; + if (clipPlane.Raycast(ray, out dist)) + { + worldPosB = ray.GetPoint(dist); + } + } + + Vector3 screenPosA = cam.WorldToViewportPoint(worldPosA); + screenPosA.x = screenPosA.x * Screen.width; + screenPosA.y = (1 - screenPosA.y) * Screen.height; + Vector3 screenPosB = cam.WorldToViewportPoint(worldPosB); + screenPosB.x = screenPosB.x * Screen.width; + screenPosB.y = (1 - screenPosB.y) * Screen.height; + + screenPosA.z = screenPosB.z = 0; + + float angle = Vector2.Angle(Vector3.up, screenPosB - screenPosA); + if (screenPosB.x < screenPosA.x) + { + angle = -angle; + } + + Vector2 vector = screenPosB - screenPosA; + float length = vector.magnitude; + + Rect upRect = new Rect(screenPosA.x - (width / 2), screenPosA.y - length, width, length); + + GUIUtility.RotateAroundPivot(-angle + 180, screenPosA); + DrawRectangle(upRect, color); + GUI.matrix = Matrix4x4.identity; + } + + public static void DrawRectangle(Rect rect, Color color) + { + if (pixel == null) + { + pixel = new Texture2D(1, 1); + } + + Color originalColor = GUI.color; + GUI.color = color; + GUI.DrawTexture(rect, pixel); + GUI.color = originalColor; + } + + public static void MarkPosition(Transform transform, Color color) => MarkPosition(transform.position, transform, color); + + public static void MarkPosition(Vector3 position, Transform transform, Color color, float size = 3, float thickness = 2) + { + DrawLineBetweenWorldPositions(position + transform.right * size, position - transform.right * size, thickness, color); + DrawLineBetweenWorldPositions(position + transform.up * size, position - transform.up * size, thickness, color); + DrawLineBetweenWorldPositions(position + transform.forward * size, position - transform.forward * size, thickness, color); + } + + public static void UseMouseEventInRect(Rect rect) + { + if (Event.current == null) return; + if (GUIUtils.MouseIsInRect(rect) && ((Event.current.isMouse && (Event.current.type == EventType.MouseDown || Event.current.type == EventType.MouseUp)) || Event.current.isScrollWheel)) + { + Event.current.Use(); + } + } + + public static Rect CleanRectVals(Rect rect) + { + // Remove decimal places so Mac does not complain. + rect.x = (int)rect.x; + rect.y = (int)rect.y; + rect.width = (int)rect.width; + rect.height = (int)rect.height; + return rect; + } + + internal static void RepositionWindow(ref Rect windowPosition) + { + if (BDArmorySettings.STRICT_WINDOW_BOUNDARIES) + { + // This method uses Gui point system. + if (windowPosition.x < 0) windowPosition.x = 0; + if (windowPosition.y < 0) windowPosition.y = 0; + + if (windowPosition.xMax > Screen.width) // Don't go off the right of the screen. + windowPosition.x = Screen.width - windowPosition.width; + if (windowPosition.height > Screen.height) // Don't go off the top of the screen. + windowPosition.y = 0; + else if (windowPosition.yMax > Screen.height) // Don't go off the bottom of the screen. + windowPosition.y = Screen.height - windowPosition.height; + } + else // If the window is completely off-screen, bring it just onto the screen. + { + if (windowPosition.width == 0) windowPosition.width = 1; + if (windowPosition.height == 0) windowPosition.height = 1; + if (windowPosition.x >= Screen.width) windowPosition.x = Screen.width - 1; + if (windowPosition.y >= Screen.height) windowPosition.y = Screen.height - 1; + if (windowPosition.x + windowPosition.width < 1) windowPosition.x = 1 - windowPosition.width; + if (windowPosition.y + windowPosition.height < 1) windowPosition.y = 1 - windowPosition.height; + } + GUIUtilsInstance.Reset(); // Reset once-per-frame checks. + } + + internal static Rect GuiToScreenRect(Rect rect) + { + // Must run during OnGui to work... + Rect newRect = new Rect + { + position = GUIUtility.GUIToScreenPoint(rect.position), + width = rect.width, + height = rect.height + }; + return newRect; + } + + public static Texture2D resizeTexture + { + get + { + if (_resizeTexture == null) _resizeTexture = GameDatabase.Instance.GetTexture(BDArmorySetup.textureDir + "resizeSquare", false); + return _resizeTexture; + } + } + static Texture2D _resizeTexture; + + public static Color ParseColor255(string color) + { + Color outputColor = new Color(0, 0, 0, 1); + + string[] strings = color.Split(","[0]); + for (int i = 0; i < 4; i++) + { + outputColor[i] = Mathf.Clamp01(Single.Parse(strings[i]) / 255); + } + + return outputColor; + } + + public static AnimationState[] SetUpAnimation(string animationName, Part part, bool animatePhysics = true) //Thanks Majiir! + { + List states = new List(); + using (IEnumerator animation = part.FindModelAnimators(animationName).AsEnumerable().GetEnumerator()) + while (animation.MoveNext()) + { + if (animation.Current == null) continue; + animation.Current.animatePhysics = animatePhysics; + AnimationState animationState = animation.Current[animationName]; + animationState.speed = 0; // FIXME Shouldn't this be 1? + animationState.enabled = true; + animationState.wrapMode = WrapMode.ClampForever; + animation.Current.Blend(animationName); + states.Add(animationState); + } + return states.ToArray(); + } + + public static AnimationState SetUpSingleAnimation(string animationName, Part part, bool animatePhysics = true) + { + using (IEnumerator animation = part.FindModelAnimators(animationName).AsEnumerable().GetEnumerator()) + while (animation.MoveNext()) + { + if (animation.Current == null) continue; + animation.Current.animatePhysics = animatePhysics; + AnimationState animationState = animation.Current[animationName]; + animationState.speed = 0; // FIXME Shouldn't this be 1? + animationState.enabled = true; + animationState.wrapMode = WrapMode.ClampForever; + animation.Current.Blend(animationName); + return animationState; + } + return null; + } + + public static bool CheckMouseIsOnGui() + { + if (!BDArmorySetup.GAME_UI_ENABLED) return false; + + if (!BDInputSettingsFields.WEAP_FIRE_KEY.inputString.Contains("mouse")) return false; + + if (ModIntegration.MouseAimFlight.IsMouseAimActive) return false; + + return GUIUtilsInstance.fetch.mouseIsOnGUI; + } + + static bool _CheckMouseIsOnGui() + { + Vector3 inverseMousePos = new Vector3(Input.mousePosition.x, Screen.height - Input.mousePosition.y, 0); + Rect topGui = new Rect(0, 0, Screen.width, 65); + + if (topGui.Contains(inverseMousePos)) return true; + if (BDArmorySetup.windowBDAToolBarEnabled && BDArmorySetup.WindowRectToolbar.Contains(inverseMousePos)) + return true; + if (ModuleTargetingCamera.windowIsOpen && BDArmorySetup.WindowRectTargetingCam.Contains(inverseMousePos)) + return true; + if (BDArmorySetup.Instance.ActiveWeaponManager) + { + MissileFire wm = BDArmorySetup.Instance.ActiveWeaponManager; + + if (wm.vesselRadarData && wm.vesselRadarData.guiEnabled) + { + if (BDArmorySetup.WindowRectRadar.Contains(inverseMousePos)) return true; + if (wm.vesselRadarData.linkWindowOpen && wm.vesselRadarData.linkWindowRect.Contains(inverseMousePos)) + return true; + } + if (wm.rwr && wm.rwr.rwrEnabled && wm.rwr.displayRWR && BDArmorySetup.WindowRectRwr.Contains(inverseMousePos)) + return true; + if (wm.wingCommander && wm.wingCommander.showGUI) + { + if (BDArmorySetup.WindowRectWingCommander.Contains(inverseMousePos)) return true; + if (wm.wingCommander.showAGWindow && wm.wingCommander.agWindowRect.Contains(inverseMousePos)) + return true; + } + + } + if (extraGUIRects != null) + { + foreach (var guiRect in extraGUIRects.Values) + { + if (!guiRect.visible) continue; + if (guiRect.rect.Contains(inverseMousePos)) return true; + } + } + + return false; + } + + public static void ResizeGuiWindow(Rect windowrect, Vector2 mousePos) + { + GUIUtilsInstance.Reset(); + } + + public class ExtraGUIRect + { + public ExtraGUIRect(Rect rect) { this.rect = rect; } + public bool visible = false; + public Rect rect; + } + public static Dictionary extraGUIRects; + + public static int RegisterGUIRect(Rect rect) + { + if (extraGUIRects == null) + { + extraGUIRects = new Dictionary(); + } + + int index = extraGUIRects.Count; + extraGUIRects.Add(index, new ExtraGUIRect(rect)); + GUIUtilsInstance.Reset(); + return index; + } + + public static void UpdateGUIRect(Rect rect, int index) + { + if (extraGUIRects == null || !extraGUIRects.ContainsKey(index)) return; + extraGUIRects[index].rect = rect; + GUIUtilsInstance.Reset(); + } + + public static void SetGUIRectVisible(int index, bool visible) + { + if (extraGUIRects == null || !extraGUIRects.ContainsKey(index)) return; + extraGUIRects[index].visible = visible; + GUIUtilsInstance.Reset(); + } + + public static bool MouseIsInRect(Rect rect) + { + Vector3 inverseMousePos = new Vector3(Input.mousePosition.x, Screen.height - Input.mousePosition.y, 0); + return rect.Contains(inverseMousePos); + } + + //Thanks FlowerChild + //refreshes part action window + public static void RefreshAssociatedWindows(Part part) + { + if (part == null || part.PartActionWindow == null) return; + part.PartActionWindow.UpdateWindow(); + // part.PartActionWindow.displayDirty = true; + // IEnumerator window = Object.FindObjectsOfType(typeof(UIPartActionWindow)).Cast().GetEnumerator(); + // while (window.MoveNext()) + // { + // if (window.Current == null) continue; + // if (window.Current.part == part) + // { + // window.Current.displayDirty = true; + // } + // } + // window.Dispose(); + } + + + /// + /// Disable zooming with the scroll wheel if the mouse is over a registered GUI window. + /// + public static void SetScrollZoom() + { + if (CheckMouseIsOnGui()) BeginDisableScrollZoom(); + else EndDisableScrollZoom(); + } + static bool scrollZoomEnabled = true; + static float originalScrollRate = 1; + static bool _originalScrollRateSet = false; + public static void BeginDisableScrollZoom() + { + if (!scrollZoomEnabled || !BDArmorySettings.SCROLL_ZOOM_PREVENTION) return; + if (!_originalScrollRateSet) + { + originalScrollRate = GameSettings.AXIS_MOUSEWHEEL.primary.scale; // Get the original scroll rate once. + if (originalScrollRate == 0) + { + Debug.LogWarning($"[BDArmory.GUIUtils]: Original scroll rate was 0, resetting it to 1."); + originalScrollRate = 1; // Sometimes it's getting set to 0 for some reason. Default it back to 1. + } + _originalScrollRateSet = true; + } + GameSettings.AXIS_MOUSEWHEEL.primary.scale = 0; + scrollZoomEnabled = false; + } + public static void EndDisableScrollZoom() + { + if (scrollZoomEnabled) return; + if (_originalScrollRateSet) + GameSettings.AXIS_MOUSEWHEEL.primary.scale = originalScrollRate; + scrollZoomEnabled = true; + } + /// + /// Reset the scroll rate to 1. + /// + public static void ResetScrollRate() + { + EndDisableScrollZoom(); + originalScrollRate = 1; + GameSettings.AXIS_MOUSEWHEEL.primary.scale = originalScrollRate; + _originalScrollRateSet = true; + scrollZoomEnabled = true; + } + + /// + /// GUILayout TextField with a grey placeholder string. + /// + /// The current text. + /// A placeholder text for when 'text' is empty. + /// An internal name for the field so it can be reference with, for example, GUI.FocusControl. + /// If specified, then GUI.TextField is used with the specified Rect, otherwise a GUILayout is used. + /// The current text. + public static string TextField(string text, string placeholder, string fieldName = null, Rect rect = default) + { + bool isGUILayout = rect == default; + if (fieldName != null) GUI.SetNextControlName(fieldName); + var newText = isGUILayout ? GUILayout.TextField(text) : GUI.TextField(rect, text); + if (String.IsNullOrEmpty(text)) + { + var guiColor = GUI.color; + GUI.color = Color.grey; + GUI.Label(isGUILayout ? GUILayoutUtility.GetLastRect() : rect, placeholder); + GUI.color = guiColor; + } + return newText; + } + + [KSPAddon(KSPAddon.Startup.EveryScene, false)] + internal class GUIUtilsInstance : MonoBehaviour + { + public bool mouseIsOnGUI + { + get + { + if (!_mouseIsOnGUICheckedThisFrame) + { + _mouseIsOnGUI = GUIUtils._CheckMouseIsOnGui(); + _mouseIsOnGUICheckedThisFrame = true; + } + return _mouseIsOnGUI; + } + } + bool _mouseIsOnGUI = false; + bool _mouseIsOnGUICheckedThisFrame = false; + + public static GUIUtilsInstance fetch; + void Awake() + { + if (fetch != null) Destroy(this); + fetch = this; + } + + void Update() + { + _mouseIsOnGUICheckedThisFrame = false; + } + + void LateUpdate() + { + SetScrollZoom(); + } + + public static void Reset() + { + if (fetch == null) return; + fetch.Update(); + } + + void Destroy() + { + GUIUtils.EndDisableScrollZoom(); + } + } + } +} diff --git a/BDArmory/Misc/KSPForceApplier.cs b/BDArmory/Utils/KSPForceApplier.cs similarity index 98% rename from BDArmory/Misc/KSPForceApplier.cs rename to BDArmory/Utils/KSPForceApplier.cs index c803985cf..f298ae6b8 100644 --- a/BDArmory/Misc/KSPForceApplier.cs +++ b/BDArmory/Utils/KSPForceApplier.cs @@ -1,6 +1,6 @@ using UnityEngine; -namespace BDArmory.Misc +namespace BDArmory.Utils { public class KSPForceApplier : MonoBehaviour { diff --git a/BDArmory/Utils/KrakensbaneUtils.cs b/BDArmory/Utils/KrakensbaneUtils.cs new file mode 100644 index 000000000..46d516434 --- /dev/null +++ b/BDArmory/Utils/KrakensbaneUtils.cs @@ -0,0 +1,102 @@ +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace BDArmory.Utils +{ + /// + /// Optimised access to Krakensbane and FloatingOrigin adjustments. + /// Krakensbane corrections happen some time between the Late and BetterLateThanNever timing phases (which both occur after the FlightIntegrator phase) (I'm reasonably sure that they occur during the Late phase). + /// If you need access to these adjustments during the BetterLateThanNever phase, then use Krakensbane or FloatingOrigin directly to ensure order of operations. + /// + /// With agressive inling, this reduces access time by a factor of ~4 for frequent access per frame. + /// Note: the access time is already quite small, but these are used frequently every frame (e.g., for bullets, rockets, explosions and countermeasures). + /// + public static class BDKrakensbane + { + public static Vector3 FrameVelocityV3f => BDKrakensbaneSingleton.Instance.GetFrameVelocityV3f; + public static Vector3d FloatingOriginOffset => BDKrakensbaneSingleton.Instance.FloatingOriginOffset; + public static Vector3d FloatingOriginOffsetNonKrakensbane => BDKrakensbaneSingleton.Instance.FloatingOriginOffsetNonKrakensbane; + public static bool IsActive => BDKrakensbaneSingleton.Instance.IsActive; + } + + [KSPAddon(KSPAddon.Startup.Flight, false)] + public class BDKrakensbaneSingleton : MonoBehaviour + { + public static BDKrakensbaneSingleton Instance; + void Awake() + { + if (Instance != null) Destroy(this); + Instance = this; + } + void OnDestroy() + { TimingManager.FixedUpdateRemove(TimingManager.TimingStage.BetterLateThanNever, Reset); } + + public Vector3 GetFrameVelocityV3f + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + if (_frameVelocityCheckedThisFrame) return _frameVelocityV3f; + _frameVelocityV3f = Krakensbane.GetFrameVelocityV3f(); + _frameVelocityCheckedThisFrame = true; + return _frameVelocityV3f; + } + } + Vector3 _frameVelocityV3f = default; + bool _frameVelocityCheckedThisFrame = false; + + public Vector3d FloatingOriginOffset + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + if (_floatingOriginOffsetCheckedThisFrame) return _floatingOriginOffset; + _floatingOriginOffset = FloatingOrigin.Offset; + _floatingOriginOffsetCheckedThisFrame = true; + return _floatingOriginOffset; + } + } + Vector3d _floatingOriginOffset = default; + bool _floatingOriginOffsetCheckedThisFrame = false; + + public Vector3d FloatingOriginOffsetNonKrakensbane + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + if (_floatingOriginOffsetNonKrakensbaneCheckedThisFrame) return _floatingOriginOffsetNonKrakensbane; + _floatingOriginOffsetNonKrakensbane = FloatingOrigin.OffsetNonKrakensbane; + _floatingOriginOffsetNonKrakensbaneCheckedThisFrame = true; + return _floatingOriginOffsetNonKrakensbane; + } + } + Vector3d _floatingOriginOffsetNonKrakensbane = default; + bool _floatingOriginOffsetNonKrakensbaneCheckedThisFrame = false; + + public bool IsActive + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + if (_isActiveCheckedThisFrame) return isActive; + isActive = !FloatingOriginOffset.IsZero() || !GetFrameVelocityV3f.IsZero(); + _isActiveCheckedThisFrame = true; + return isActive; + } + } + bool isActive = false; + bool _isActiveCheckedThisFrame = false; + + void Start() + { + TimingManager.FixedUpdateAdd(TimingManager.TimingStage.BetterLateThanNever, Reset); // Reset the flags before anything else runs. + } + void Reset() + { + _frameVelocityCheckedThisFrame = false; + _floatingOriginOffsetCheckedThisFrame = false; + _floatingOriginOffsetNonKrakensbaneCheckedThisFrame = false; + _isActiveCheckedThisFrame = false; + } + } +} \ No newline at end of file diff --git a/BDArmory.Core/Utils/LayerMask.cs b/BDArmory/Utils/LayerMask.cs similarity index 91% rename from BDArmory.Core/Utils/LayerMask.cs rename to BDArmory/Utils/LayerMask.cs index bb3ae285e..5a22172c8 100644 --- a/BDArmory.Core/Utils/LayerMask.cs +++ b/BDArmory/Utils/LayerMask.cs @@ -1,4 +1,4 @@ -namespace BDArmory.Core.Utils +namespace BDArmory.Utils { internal class LayerMask { @@ -29,10 +29,12 @@ public enum LayerMasks { Parts = 1 << 0, Scenery = 1 << 15, + Kerbals = 1 << 16, // Internal kerbals EVA = 1 << 17, Unknown19 = 1 << 19, // Why are some raycasts using this layer? RootPart = 1 << 21, - Unknown23 = 1 << 23 // Why are some raycasts using this layer? + Unknown23 = 1 << 23, // Why are some raycasts using this layer? + Wheels = 1 << 26 }; // Scenery includes terrain and buildings. // Commonly used values: // 163840 = (1 << 15) | (1 << 17) diff --git a/BDArmory/Misc/ObjectPool.cs b/BDArmory/Utils/ObjectPool.cs similarity index 89% rename from BDArmory/Misc/ObjectPool.cs rename to BDArmory/Utils/ObjectPool.cs index 345382f73..b8176d074 100644 --- a/BDArmory/Misc/ObjectPool.cs +++ b/BDArmory/Utils/ObjectPool.cs @@ -1,9 +1,10 @@ using System.Collections; using System.Collections.Generic; using UnityEngine; -using BDArmory.Core; -namespace BDArmory.Misc +using BDArmory.Settings; + +namespace BDArmory.Utils { public class ObjectPool : MonoBehaviour { @@ -14,7 +15,7 @@ public class ObjectPool : MonoBehaviour public bool forceReUse; public int lastIndex = 0; - List pool; + public List pool; public string poolObjectName; @@ -49,7 +50,7 @@ public void AdjustSize(int count) pool.RemoveRange(count, size - count); lastIndex = 0; } - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.ObjectPool]: Resizing " + poolObjectName + " pool to " + size); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log("[BDArmory.ObjectPool]: Resizing " + poolObjectName + " pool to " + size); } private void AddObjectsToPool(int count) @@ -105,7 +106,7 @@ public GameObject GetPooledObject() if (canGrow) { var size = (int)(pool.Count * 1.2) + 1; // Grow by 20% + 1 - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.ObjectPool]: Increasing pool size to " + size + " for " + poolObjectName); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log("[BDArmory.ObjectPool]: Increasing pool size to " + size + " for " + poolObjectName); AddObjectsToPool(size - pool.Count); if (disableAfterDelay > 0f) DisableAfterDelay(pool[pool.Count - 1], disableAfterDelay); @@ -130,7 +131,7 @@ public void DisableAfterDelay(GameObject obj, float t) IEnumerator DisableObject(GameObject obj, float t) { - yield return new WaitForSeconds(t); + yield return new WaitForSecondsFixed(t); if (obj) { obj.SetActive(false); @@ -140,7 +141,7 @@ IEnumerator DisableObject(GameObject obj, float t) public static ObjectPool CreateObjectPool(GameObject obj, int size, bool canGrow, bool destroyOnLoad, float disableAfterDelay = 0f, bool forceReUse = false) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.ObjectPool]: Creating object pool of size " + size + " for " + obj.name); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log("[BDArmory.ObjectPool]: Creating object pool of size " + size + " for " + obj.name); GameObject poolObject = new GameObject(obj.name + "Pool"); ObjectPool op = poolObject.AddComponent(); op.poolObject = obj; diff --git a/BDArmory/Utils/ObjectPoolNonUnity.cs b/BDArmory/Utils/ObjectPoolNonUnity.cs new file mode 100644 index 000000000..8b8cf3a64 --- /dev/null +++ b/BDArmory/Utils/ObjectPoolNonUnity.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +using BDArmory.Settings; + +namespace BDArmory.Utils +{ + public class ObjectPoolEntry where T : new() + { + public T value; + public bool inUse = false; // Set this once you're done with the entry. + public ObjectPoolEntry() { value = new T(); } + } + + public class ObjectPoolNonUnity where T : new() + { + int lastIndex = 0; + + public List> pool = new List>(); + + public ObjectPoolNonUnity(int size = 10) + { + if (BDArmorySettings.DEBUG_OTHER) Debug.Log("[BDArmory.ObjectPoolNonUnity]: Creating object pool of size " + size + " for " + typeof(T)); + AddObjectsToPool(size); + } + + private void AddObjectsToPool(int count) + { + for (int i = 0; i < count; ++i) + { + pool.Add(new ObjectPoolEntry()); + } + } + + private void ReplacePoolObject(int index) + { + Debug.LogWarning("[BDArmory.ObjectPoolNonUnity]: Object of type " + typeof(T) + " was null at position " + index + ", replacing it."); + pool[index] = new ObjectPoolEntry(); + } + + public ObjectPoolEntry GetPooledObject() + { + // Start at the last index returned and cycle round for efficiency. This makes this a typically O(1) seek operation. + for (int i = lastIndex + 1; i < pool.Count; ++i) + { + if (pool[i].value == null) + { + ReplacePoolObject(i); + } + if (!pool[i].inUse) + { + lastIndex = i; + pool[i].inUse = true; + return pool[i]; + } + } + for (int i = 0; i < lastIndex + 1; ++i) + { + if (pool[i].value == null) + { + ReplacePoolObject(i); + } + if (!pool[i].inUse) + { + lastIndex = i; + pool[i].inUse = true; + return pool[i]; + } + } + + // The pool is full, increase it by 20%+1 and return the last entry. + var size = (int)(pool.Count * 1.2) + 1; + if (BDArmorySettings.DEBUG_OTHER) Debug.Log("[BDArmory.ObjectPoolNonUnity]: Increasing pool size to " + size + " for " + typeof(T)); + AddObjectsToPool(size - pool.Count); + pool[pool.Count - 1].inUse = true; + return pool[pool.Count - 1]; + } + } +} diff --git a/BDArmory/Utils/OtherUtils.cs b/BDArmory/Utils/OtherUtils.cs new file mode 100644 index 000000000..02d5a6762 --- /dev/null +++ b/BDArmory/Utils/OtherUtils.cs @@ -0,0 +1,201 @@ +using BDArmory.Settings; +using UnityEngine; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace BDArmory.Utils +{ + public static class OtherUtils // FIXME Suggestions for a better name? + { + /// + /// Parses the string to a curve. + /// Format: "key:pair,key:pair" + /// + /// The curve. + /// Curve string. + public static FloatCurve ParseCurve(string curveString) + { + string[] pairs = curveString.Split(new char[] { ',' }); + Keyframe[] keys = new Keyframe[pairs.Length]; + for (int p = 0; p < pairs.Length; p++) + { + string[] pair = pairs[p].Split(new char[] { ':' }); + keys[p] = new Keyframe(float.Parse(pair[0]), float.Parse(pair[1])); + } + + FloatCurve curve = new FloatCurve(keys); + + return curve; + } + + public static IEnumerable GetLoadableTypes(this Assembly assembly) + { + // TODO: Argument validation + try + { + return assembly.GetTypes(); + } + catch (ReflectionTypeLoadException e) + { + return e.Types.Where(t => t != null); + } + } + + private const int lineOfSightLayerMask = (int)(LayerMasks.Parts | LayerMasks.Scenery | LayerMasks.EVA | LayerMasks.Unknown19 | LayerMasks.Unknown23 | LayerMasks.Wheels); + public static bool CheckSightLine(Vector3 origin, Vector3 target, float maxDistance, float threshold, + float startDistance) + { + float dist = maxDistance; + Ray ray = new Ray(origin, target - origin); + ray.origin += ray.direction * startDistance; + RaycastHit rayHit; + if (Physics.Raycast(ray, out rayHit, dist, lineOfSightLayerMask)) + { + if ((target - rayHit.point).sqrMagnitude < threshold * threshold) + { + return true; + } + else + { + return false; + } + } + + return false; + } + + public static bool CheckSightLineExactDistance(Vector3 origin, Vector3 target, float maxDistance, + float threshold, float startDistance) + { + float dist = maxDistance; + Ray ray = new Ray(origin, target - origin); + ray.origin += ray.direction * startDistance; + RaycastHit rayHit; + + if (Physics.Raycast(ray, out rayHit, dist, lineOfSightLayerMask)) + { + if ((target - rayHit.point).sqrMagnitude < threshold * threshold) + { + return true; + } + else + { + return false; + } + } + + return true; + } + + public static float[] ParseToFloatArray(string floatString) + { + string[] floatStrings = floatString.Split(new char[] { ',' }); + float[] floatArray = new float[floatStrings.Length]; + for (int i = 0; i < floatStrings.Length; i++) + { + floatArray[i] = float.Parse(floatStrings[i]); + } + + return floatArray; + } + + public static KeyBinding AGEnumToKeybinding(KSPActionGroup group) + { + string groupName = group.ToString(); + if (groupName.Contains("Custom")) + { + groupName = groupName.Substring(6); + int customNumber = int.Parse(groupName); + groupName = "CustomActionGroup" + customNumber; + } + else + { + return null; + } + + FieldInfo field = typeof(GameSettings).GetField(groupName); + return (KeyBinding)field.GetValue(null); + } + + public static string JsonCompat(string json) + { + return json.Replace('{', '<').Replace('}', '>'); + } + + public static string JsonDecompat(string json) + { + return json.Replace('<', '{').Replace('>', '}'); + } + + public static void SetTimeOverride(bool enabled) + { + BDArmorySettings.TIME_OVERRIDE = enabled; + Time.timeScale = enabled ? BDArmorySettings.TIME_SCALE : 1f; + } + } + + /// + /// Custom yield instruction that allows waiting for a number of seconds based on the FixedUpdate cycle instead of the Update cycle. + /// Based on http://answers.unity.com/comments/1910230/view.html + /// + /// Note: All Unity yield instructions other than WaitForFixedUpdate wait until the next Update cycle to check their conditions, including "yield return null". + /// For any yielding that is physics related, use WaitForFixedUpdate (use a single instance and yield it multiple times) or one of the classes below. + /// + public class WaitForSecondsFixed : IEnumerator + { + private WaitForFixedUpdate wait = new WaitForFixedUpdate(); + public virtual object Current => this.wait; + float endTime, seconds; + + public WaitForSecondsFixed(float seconds) + { + this.seconds = seconds; + this.Reset(); + } + + public bool MoveNext() => this.keepWaiting; + public virtual bool keepWaiting => (Time.fixedTime < endTime); + public virtual void Reset() => this.endTime = Time.fixedTime + this.seconds; + } + + /// + /// Custom yield instruction that allows yielding until a predicate is satisfied based on the FixedUpdate cycle instead of the Update cycle. + /// + public class WaitUntilFixed : IEnumerator + { + private WaitForFixedUpdate wait = new WaitForFixedUpdate(); + public virtual object Current => wait; + Func predicate; + + public WaitUntilFixed(Func predicate) + { + this.predicate = predicate; + } + + public bool MoveNext() => !predicate(); + public virtual void Reset() { } + } + + /// + /// Custom yield instruction that allows yielding while a predicate is satisfied based on the FixedUpdate cycle instead of the Update cycle. + /// + public class WaitWhileFixed : IEnumerator + { + private WaitForFixedUpdate wait = new WaitForFixedUpdate(); + public virtual object Current => wait; + Func predicate; + + public WaitWhileFixed(Func predicate) + { + this.predicate = predicate; + } + + public bool MoveNext() => predicate(); + public virtual void Reset() { } + } + + public enum Toggle { On, Off, Toggle, NoChange }; // Turn something on, off, toggle it or leave it as it is. +} \ No newline at end of file diff --git a/BDArmory/Utils/PartExploderSystem.cs b/BDArmory/Utils/PartExploderSystem.cs new file mode 100644 index 000000000..6f5a3ff5e --- /dev/null +++ b/BDArmory/Utils/PartExploderSystem.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +using BDArmory.Extensions; +using BDArmory.Settings; + +namespace BDArmory.Utils +{ + [KSPAddon(KSPAddon.Startup.Flight, false)] + public class PartExploderSystem : MonoBehaviour + { + private static readonly HashSet ExplodingParts = new HashSet(); + private static List nowExploding = new List(); + + public static void AddPartToExplode(Part p) + { + if (!HighLogic.LoadedSceneIsFlight) return; + if (p == null) return; + ExplodingParts.Add(p); + } + + private void OnDestroy() + { + ExplodingParts.Clear(); + } + + public void Update() + { + if (ExplodingParts.Count == 0) return; + + do + { + ExplodingParts.Remove(null); // Clear out any null parts. + ExplodingParts.RemoveWhere(p => p.packed || (p.vessel is not null && !p.vessel.loaded)); // Remove parts that are already gone. + nowExploding = ExplodingParts.Where(p => !ExplodingParts.Contains(p.parent)).ToList(); // Explode outer-most parts first to avoid creating new vessels needlessly. + foreach (var part in nowExploding) + { + part.explode(); + ExplodingParts.Remove(part); + } + } while (ExplodingParts.Count > 0); + } + } +} diff --git a/BDArmory/Misc/ProjectileUtils.cs b/BDArmory/Utils/ProjectileUtils.cs similarity index 57% rename from BDArmory/Misc/ProjectileUtils.cs rename to BDArmory/Utils/ProjectileUtils.cs index 672383e99..0e08b1276 100644 --- a/BDArmory/Misc/ProjectileUtils.cs +++ b/BDArmory/Utils/ProjectileUtils.cs @@ -4,74 +4,184 @@ using UnityEngine; using BDArmory.Competition; -using BDArmory.Core.Extension; -using BDArmory.Core.Module; -using BDArmory.Core; +using BDArmory.Damage; +using BDArmory.Extensions; using BDArmory.FX; using BDArmory.GameModes; +using BDArmory.Settings; +using System.IO; -namespace BDArmory.Misc +namespace BDArmory.Utils { class ProjectileUtils { - static HashSet FuelResources + public static string settingsConfigURL = Path.Combine(KSPUtil.ApplicationRootPath, "GameData/BDArmory/PluginData/PartsBlacklists.cfg"); + public static void SetUpPartsHashSets() { - get + var fileNode = ConfigNode.Load(settingsConfigURL); + if (fileNode == null) { - if (_FuelResources == null) + fileNode = new ConfigNode(); + if (!Directory.GetParent(settingsConfigURL).Exists) + { Directory.GetParent(settingsConfigURL).Create(); } + } + // IgnoredParts + { + if (!fileNode.HasNode("IgnoredParts")) + { + fileNode.AddNode("IgnoredParts"); + } + ConfigNode Iparts = fileNode.GetNode("IgnoredParts"); + var partNames = Iparts.GetValues().ToHashSet(); // Get the existing part names, then add our ones. + partNames.Add("ladder1"); + partNames.Add("telescopicLadder"); + partNames.Add("telescopicLadderBay"); + Iparts.ClearValues(); + int partIndex = 0; + foreach (var partName in partNames) + Iparts.SetValue($"Part{++partIndex}", partName, true); + } + // MaterialsBlacklist + { + if (!fileNode.HasNode("MaterialsBlacklist")) { - _FuelResources = new HashSet(); - foreach (var resource in PartResourceLibrary.Instance.resourceDefinitions) + fileNode.AddNode("MaterialsBlacklist"); + } + ConfigNode BLparts = fileNode.GetNode("MaterialsBlacklist"); + var partNames = BLparts.GetValues().ToHashSet(); // Get the existing part names, then add our ones. + partNames.Add("InflatableHeatShield"); + partNames.Add("foldingRad*"); + partNames.Add("radPanel*"); + partNames.Add("ISRU*"); + partNames.Add("Scanner*"); + partNames.Add("Drill*"); + partNames.Add("PotatoRoid"); + BLparts.ClearValues(); + int partIndex = 0; + foreach (var partName in partNames) + BLparts.SetValue($"Part{++partIndex}", partName, true); + } + fileNode.Save(settingsConfigURL); + } + static HashSet IgnoredPartNames; + public static bool IsIgnoredPart(Part part) + { + if (IgnoredPartNames == null) + { + IgnoredPartNames = new HashSet { "bdPilotAI", "bdShipAI", "missileController", "bdammGuidanceModule" }; + IgnoredPartNames.UnionWith(PartLoader.LoadedPartsList.Select(p => p.partPrefab.partInfo.name).Where(name => name.Contains("flag"))); + IgnoredPartNames.UnionWith(PartLoader.LoadedPartsList.Select(p => p.partPrefab.partInfo.name).Where(name => name.Contains("conformaldecals"))); + + var fileNode = ConfigNode.Load(settingsConfigURL); + if (fileNode.HasNode("IgnoredParts")) + { + ConfigNode parts = fileNode.GetNode("IgnoredParts"); + //Debug.Log($"[BDArmory.ProjectileUtils]: partsBlacklist.cfg IgnoredParts count: " + parts.CountValues); + for (int i = 0; i < parts.CountValues; i++) { - if (resource.name.EndsWith("Fuel") || resource.name.EndsWith("Oxidizer") || resource.name.EndsWith("Air") || resource.name.EndsWith("Charge") || resource.name.EndsWith("Gas") || resource.name.EndsWith("Propellant")) // FIXME These ought to be configurable - { _FuelResources.Add(resource.name); } + if (parts.values[i].value.Contains("*")) + { + string partsName = parts.values[i].value.Trim('*'); + IgnoredPartNames.UnionWith(PartLoader.LoadedPartsList.Select(p => p.partPrefab.partInfo.name).Where(name => name.Contains(partsName))); + } + else + IgnoredPartNames.Add(parts.values[i].value); } - Debug.Log("[BDArmory.ProjectileUtils]: Fuel resources: " + string.Join(", ", _FuelResources)); } - return _FuelResources; + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log($"[BDArmory.ProjectileUtils]: Ignored Parts: " + string.Join(", ", IgnoredPartNames)); + } + return ProjectileUtils.IgnoredPartNames.Contains(part.partInfo.name); + } + static HashSet armorParts; + public static bool IsArmorPart(Part part) + { + if (BDArmorySettings.LEGACY_ARMOR) return false; + if (armorParts == null) + { + armorParts = PartLoader.LoadedPartsList.Select(p => p.partPrefab.partInfo.name).Where(name => name.ToLower().Contains("armor")).ToHashSet(); + if (BDArmorySettings.DEBUG_ARMOR) Debug.Log($"[BDArmory.ProjectileUtils]: Armor Parts: " + string.Join(", ", armorParts)); } + return armorParts.Contains(part.partInfo.name); } - static HashSet _FuelResources; - static HashSet AmmoResources + public static void SetUpWeaponReporting() { - get + var fileNode = ConfigNode.Load(settingsConfigURL); + if (fileNode == null) // Note: this shouldn't happen since SetUpPartsHashSets is called before SetUpWeaponReporting. { - if (_AmmoResources == null) + SetUpPartsHashSets(); + fileNode = ConfigNode.Load(settingsConfigURL); + } + + string announcerGunsComment = "Note: replace '_' with '.' in part names (hint: see a craft's loadmeta file for part names)."; // Note: reading the node doesn't seem to get the comment, so we need to reset it each time. + if (!fileNode.HasNode("AnnouncerGuns")) + { + fileNode.AddNode("AnnouncerGuns", announcerGunsComment); + } + ConfigNode Iparts = fileNode.GetNode("AnnouncerGuns"); + Iparts.comment = announcerGunsComment; + var partNames = Iparts.GetValues().ToHashSet(); // Get the existing part names, then add our ones. + partNames.Add("bahaRailgun"); + Iparts.ClearValues(); + int partIndex = 0; + foreach (var partName in partNames) + Iparts.SetValue($"Part{++partIndex}", partName, true); + + fileNode.Save(settingsConfigURL); + } + static HashSet materialsBlacklist; + public static bool isMaterialBlackListpart(Part Part) + { + if (materialsBlacklist == null) + { + materialsBlacklist = new HashSet { "bdPilotAI", "bdShipAI", "missileController", "bdammGuidanceModule", "PotatoRoid" }; + + var fileNode = ConfigNode.Load(settingsConfigURL); + if (fileNode.HasNode("MaterialsBlacklist")) { - _AmmoResources = new HashSet(); - foreach (var resource in PartResourceLibrary.Instance.resourceDefinitions) + ConfigNode parts = fileNode.GetNode("MaterialsBlacklist"); + //Debug.Log($"[BDArmory.ProjectileUtils]: partsBlacklist.cfg BlacklistParts count: " + parts.CountValues); + for (int i = 0; i < parts.CountValues; i++) { - if (resource.name.EndsWith("Ammo") || resource.name.EndsWith("Shell") || resource.name.EndsWith("Shells") || resource.name.EndsWith("Rocket") || resource.name.EndsWith("Rockets") || resource.name.EndsWith("Bolt") || resource.name.EndsWith("Mauser")) - { _AmmoResources.Add(resource.name); } + if (parts.values[i].value.Contains("*")) + { + string partsName = parts.values[i].value.Trim('*'); + Debug.Log($"[BDArmory.ProjectileUtils]: Found wildcard, name:" + partsName); + materialsBlacklist.UnionWith(PartLoader.LoadedPartsList.Select(p => p.partPrefab.partInfo.name).Where(name => name.Contains(partsName))); + } + else + materialsBlacklist.Add(parts.values[i].value); } - Debug.Log("[BDArmory.ProjectileUtils]: Ammo resources: " + string.Join(", ", _AmmoResources)); } - return _AmmoResources; + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log($"[BDArmory.ProjectileUtils]: Part Material blacklist: " + string.Join(", ", materialsBlacklist)); } + return ProjectileUtils.materialsBlacklist.Contains(Part.partInfo.name); } - static HashSet _AmmoResources; - static HashSet CMResources + static HashSet reportingWeaponList; + public static bool isReportingWeapon(Part Part) { - get + if (reportingWeaponList == null) { - if (_CMResources == null) + reportingWeaponList = new HashSet { }; + + var fileNode = ConfigNode.Load(settingsConfigURL); + if (fileNode.HasNode("AnnouncerGuns")) { - _CMResources = new HashSet(); - foreach (var resource in PartResourceLibrary.Instance.resourceDefinitions) + ConfigNode parts = fileNode.GetNode("AnnouncerGuns"); + for (int i = 0; i < parts.CountValues; i++) { - if (resource.name.EndsWith("Flare") || resource.name.EndsWith("Smoke") || resource.name.EndsWith("Chaff")) - { _CMResources.Add(resource.name); } + if (parts.values[i].value.Contains("*")) + { + string partsName = parts.values[i].value.Trim('*'); + reportingWeaponList.UnionWith(PartLoader.LoadedPartsList.Select(p => p.partPrefab.partInfo.name).Where(name => name.Contains(partsName))); + } + else + reportingWeaponList.Add(parts.values[i].value); } - Debug.Log("[BDArmory.ProjectileUtils]: Couter-measure resources: " + string.Join(", ", _CMResources)); } - return _CMResources; + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log($"[BDArmory.ProjectileUtils]: Weapon Reporting List: " + string.Join(", ", reportingWeaponList)); } + return ProjectileUtils.reportingWeaponList.Contains(Part.partInfo.name); } - static HashSet _CMResources; - - public static HashSet IgnoredPartNames = new HashSet { "bdPilotAI", "bdShipAI", "missileController", "bdammGuidanceModule" }; - public static bool IsIgnoredPart(Part part) { return ProjectileUtils.IgnoredPartNames.Contains(part.partInfo.name); } - public static void ApplyDamage(Part hitPart, RaycastHit hit, float multiplier, float penetrationfactor, float caliber, float projmass, float impactVelocity, float DmgMult, double distanceTraveled, bool explosive, bool incendiary, bool hasRichocheted, Vessel sourceVessel, string name, string team, ExplosionSourceType explosionSource, bool firstHit, bool partAlreadyHit, bool cockpitPen) { //hitting a vessel Part @@ -88,19 +198,19 @@ public static void ApplyDamage(Part hitPart, RaycastHit hit, float multiplier, f // Apply damage float damage; damage = hitPart.AddBallisticDamage(projmass, caliber, multiplier, penetrationfactor, DmgMult, impactVelocity, explosionSource); + if (BDArmorySettings.DEBUG_WEAPONS) Debug.Log("[BDArmory.PartExtensions]: Ballistic Hitpoints Applied to " + hitPart.name + ": " + damage); if (BDArmorySettings.BATTLEDAMAGE) { BattleDamageHandler.CheckDamageFX(hitPart, caliber, penetrationfactor, explosive, incendiary, sourceVessel.GetName(), hit, partAlreadyHit, cockpitPen); } - // Debug.Log("DEBUG Ballistic damage to " + hitPart + ": " + damage + ", calibre: " + caliber + ", multiplier: " + multiplier + ", pen: " + penetrationfactor); // Update scoring structures - if (firstHit) - { - ApplyScore(hitPart, sourceVessel.GetName(), distanceTraveled, damage, name, explosionSource, true); - } - StealResources(hitPart, sourceVessel); + //if (firstHit) + //{ + ApplyScore(hitPart, sourceVessel.GetName(), distanceTraveled, damage, name, explosionSource, firstHit); + //} + ResourceUtils.StealResources(hitPart, sourceVessel); } public static void ApplyScore(Part hitPart, string sourceVessel, double distanceTraveled, float damage, string name, ExplosionSourceType ExplosionSource, bool newhit = false) { @@ -124,16 +234,6 @@ public static void ApplyScore(Part hitPart, string sourceVessel, double distance break; } } - public static void StealResources(Part hitPart, Vessel sourceVessel, bool thiefWeapon = false) - { - // steal resources if enabled - if (BDArmorySettings.RESOURCE_STEAL_ENABLED || thiefWeapon) - { - if (BDArmorySettings.RESOURCE_STEAL_FUEL_RATION > 0f) StealResource(hitPart.vessel, sourceVessel, FuelResources, BDArmorySettings.RESOURCE_STEAL_FUEL_RATION); - if (BDArmorySettings.RESOURCE_STEAL_AMMO_RATION > 0f) StealResource(hitPart.vessel, sourceVessel, AmmoResources, BDArmorySettings.RESOURCE_STEAL_AMMO_RATION, true); - if (BDArmorySettings.RESOURCE_STEAL_CM_RATION > 0f) StealResource(hitPart.vessel, sourceVessel, CMResources, BDArmorySettings.RESOURCE_STEAL_CM_RATION, true); - } - } public static float CalculateArmorPenetration(Part hitPart, float penetration, float thickness) { /////////////////////////////////////////////////////////////////////// @@ -147,13 +247,13 @@ public static float CalculateArmorPenetration(Part hitPart, float penetration, f } var penetrationFactor = penetration / thickness; - if (BDArmorySettings.DRAW_ARMOR_LABELS) + if (BDArmorySettings.DEBUG_ARMOR) { Debug.Log("[BDArmory.ProjectileUtils{Armor Penetration}]:" + hitPart + ", " + hitPart.vessel.GetName() + ": Armor penetration = " + penetration + "mm | Thickness = " + thickness + "mm"); } if (penetrationFactor < 1) { - if (BDArmorySettings.DRAW_ARMOR_LABELS) + if (BDArmorySettings.DEBUG_ARMOR) { Debug.Log("[BDArmory.ProjectileUtils{Armor Penetration}]: Bullet Stopped by Armor"); } @@ -166,10 +266,11 @@ public static void CalculateArmorDamage(Part hitPart, float penetrationFactor, f /// Calculate damage to armor from kinetic impact based on armor mechanical properties /// Sufficient penetration by bullet will result in armor spalling or failure /// - if (!hitPart.name.ToLower().Contains("armor")) + if (!IsArmorPart(hitPart)) { if (armorType == 1) return; //ArmorType "None"; no armor to block/reduce blast, take full damage } + if (BDArmorySettings.PAINTBALL_MODE) return; //don't damage armor if paintball mode float thickness = (float)hitPart.GetArmorThickness(); if (thickness <= 0) return; //No armor present to spall/damage @@ -192,7 +293,7 @@ public static void CalculateArmorDamage(Part hitPart, float penetrationFactor, f } spallCaliber = caliber * (caliberModifier / 2); spallMass = (spallCaliber * spallCaliber * Mathf.PI / 400) * (thickness / 10) * (density / 1000000000); - if (BDArmorySettings.DRAW_ARMOR_LABELS) + if (BDArmorySettings.DEBUG_ARMOR) { Debug.Log("[BDArmory.ProjectileUtils]: " + hitPart + ", " + hitPart.vessel.GetName() + ": Armor spalling! Diameter: " + spallCaliber + "; mass: " + (spallMass * 1000) + "kg"); } @@ -211,8 +312,8 @@ public static void CalculateArmorDamage(Part hitPart, float penetrationFactor, f if (ductility < 0.05f) //ceramics { volumeToReduce = ((Mathf.CeilToInt(caliber / 500) * Mathf.CeilToInt(caliber / 500)) * (50 * 50) * ((float)hitPart.GetArmorMaxThickness() / 10)); //cm3 //replace thickness with starting thickness, to ensure armor failure removes proper amount of armor - //total failue of 50x50cm armor tile(s) - if (BDArmorySettings.DRAW_ARMOR_LABELS) + //total failue of 50x50cm armor tile(s) + if (BDArmorySettings.DEBUG_ARMOR) { Debug.Log("[BDArmory.ProjectileUtils{CalcArmorDamage}]: Armor failure on " + hitPart + ", " + hitPart.vessel.GetName() + "!"); } @@ -228,7 +329,7 @@ public static void CalculateArmorDamage(Part hitPart, float penetrationFactor, f volumeToReduce = spallCaliber; //cm3 spallMass = spallCaliber * (density / 1000000000); - if (BDArmorySettings.DRAW_ARMOR_LABELS) + if (BDArmorySettings.DEBUG_ARMOR) { Debug.Log("[BDArmory.ProjectileUtils{CalcArmorDamage}]: Armor failure on " + hitPart + ", " + hitPart.vessel.GetName() + "!"); Debug.Log("[BDArmory.ProjectileUtils{CalcArmorDamage}]: Armor spalling! Diameter: " + spallCaliber + "; mass: " + (spallMass * 1000) + "kg"); @@ -243,14 +344,14 @@ public static void CalculateArmorDamage(Part hitPart, float penetrationFactor, f var modifiedCaliber = 0.5f * caliber * caliberModifier; volumeToReduce = modifiedCaliber * modifiedCaliber * Mathf.PI / 100 * (thickness / 10); //cm3 } - if (BDArmorySettings.DRAW_ARMOR_LABELS) + if (BDArmorySettings.DEBUG_ARMOR) { Debug.Log("[BDArmory.ProjectileUtils{CalcArmorDamage}]: " + hitPart + " on " + hitPart.vessel.GetName() + " Armor volume lost: " + Math.Round(volumeToReduce) + " cm3"); } hitPart.ReduceArmor((double)volumeToReduce); if (penetrationFactor < 1) { - if (BDArmorySettings.DRAW_ARMOR_LABELS) + if (BDArmorySettings.DEBUG_ARMOR) { Debug.Log("[BDArmory.ProjectileUtils{CalcArmorDamage}]: Bullet Stopped by Armor"); } @@ -258,30 +359,31 @@ public static void CalculateArmorDamage(Part hitPart, float penetrationFactor, f if (spallMass > 0) { float damage = hitPart.AddBallisticDamage(spallMass, spallCaliber, 1, 1.1f, 1, (impactVel / 2), explosionSource); - if (BDArmorySettings.DRAW_ARMOR_LABELS) + if (BDArmorySettings.DEBUG_ARMOR) { Debug.Log("[BDArmory.ProjectileUtils]: " + hitPart + " on " + hitPart.vessel.GetName() + " takes Spall Damage: " + damage); } ApplyScore(hitPart, sourceVesselName, 0, damage, "Spalling", explosionSource); } } - public static void CalculateShrapnelDamage(Part hitPart, RaycastHit hit, float caliber, float HEmass, float detonationDist, string sourceVesselName, ExplosionSourceType explosionSource, float projmass = -1, float penetrationFactor = -1) + public static void CalculateShrapnelDamage(Part hitPart, RaycastHit hit, float caliber, float HEmass, float detonationDist, string sourceVesselName, ExplosionSourceType explosionSource, float projmass = -1, float penetrationFactor = -1, float thickness = -1) { /// /// Calculates damage from flak/shrapnel, based on HEmass and projMass, of both contact and airburst detoantions. /// Calculates # hits per m^2 based on distribution across sphere detonationDist in radius /// Shrapnel penetration dist determined by caliber, penetration. Penetration = -1 is part only hit by blast/airburst /// - float thickness = (float)hitPart.GetArmorThickness(); + if (BDArmorySettings.PAINTBALL_MODE) return; //don't damage armor if paintball mode + if (thickness < 0) thickness = (float)hitPart.GetArmorThickness(); if (thickness < 1) { - thickness = 0.1f; //prevent divide by zero or other odd behavior + thickness = 1; //prevent divide by zero or other odd behavior } double volumeToReduce = 0; var Armor = hitPart.FindModuleImplementing(); if (Armor != null) { - if (!hitPart.name.ToLower().Contains("armor")) + if (!IsArmorPart(hitPart)) { if (Armor.ArmorTypeNum == 1) return; //ArmorType "None"; no armor to block/reduce blast, take full damage } @@ -306,7 +408,7 @@ public static void CalculateShrapnelDamage(Part hitPart, RaycastHit hit, float c HERatio = Mathf.Clamp(HEmass / projmass, 0.01f, 0.95f); float frangibility = 5000 * HERatio; float shrapnelThickness = ((.0075f * Mathf.Pow((HERatio * 100), 1.05f)) + .06f) * caliber; //min thickness of material for HE to blow caliber size hole in steel - shrapnelThickness *= (950 / Strength) * (8000 / Density) * (Mathf.Sqrt(1100 / hardness)); //adjusted min thickness after material hardness/strength/density + shrapnelThickness *= (950 / Strength) * (8000 / Density) * (BDAMath.Sqrt(1100 / hardness)); //adjusted min thickness after material hardness/strength/density float shrapnelCount; float radiativeArea = !double.IsNaN(hitPart.radiativeArea) ? (float)hitPart.radiativeArea : hitPart.GetArea(); if (detonationDist > 0) @@ -323,7 +425,7 @@ public static void CalculateShrapnelDamage(Part hitPart, RaycastHit hit, float c // go through and make sure all unit conversions correct if (penetrationFactor < 0) //airburst/parts caught in AoE { - if (detonationDist > (5 * (caliber / 1000))) //caliber in mm, not m + //if (detonationDist > (5 * (caliber / 1000))) //caliber in mm, not m { if (thickness < shrapnelThickness && shrapnelCount > 0) { @@ -331,52 +433,53 @@ public static void CalculateShrapnelDamage(Part hitPart, RaycastHit hit, float c volumeToReduce = (((caliber * caliber) * 1.5f) / shrapnelCount * thickness) / 1000; //rough approximation of volume / # of fragments hitPart.ReduceArmor(volumeToReduce); - if (BDArmorySettings.DRAW_ARMOR_LABELS) + damage = hitPart.AddBallisticDamage(shrapnelMass, 0.1f, 1, (shrapnelThickness / thickness), 1, 430, explosionSource); //expansion rate of tnt/petn ~7500m/s + if (BDArmorySettings.DEBUG_ARMOR) { - Debug.Log("[BDArmory.ProjectileUtils]: " + hitPart.name + " on " + hitPart.vessel.GetName() + ", " + shrapnelCount + " shrapnel hits; Armor damage: " + volumeToReduce + "cm3; part damage: "); + Debug.Log("[BDArmory.ProjectileUtils{CalcShrapnel}]: " + hitPart.name + " on " + hitPart.vessel.GetName() + ", detonationDist: " + detonationDist + "; " + shrapnelCount + " shrapnel hits; Armor damage: " + volumeToReduce + "cm3; part damage: " + damage); } - damage = hitPart.AddBallisticDamage(shrapnelMass, 0.1f, 1, (shrapnelThickness / thickness), 1, 430, explosionSource); //expansion rate of tnt/petn ~7500m/s ApplyScore(hitPart, sourceVesselName, 0, damage, "Shrapnel", explosionSource); - CalculateArmorDamage(hitPart, (shrapnelThickness / thickness), Mathf.Sqrt((float)volumeToReduce / 3.14159f), hardness, Ductility, Density, 430, sourceVesselName, explosionSource, armorType); + CalculateArmorDamage(hitPart, (shrapnelThickness / thickness), BDAMath.Sqrt((float)volumeToReduce / 3.14159f), hardness, Ductility, Density, 430, sourceVesselName, explosionSource, armorType); BattleDamageHandler.CheckDamageFX(hitPart, caliber, (shrapnelThickness / thickness), false, false, sourceVesselName, hit); //bypass score mechanic so HE rounds don't have inflated scores } } + /* else //within 5 calibers of detonation + //a 8" (200mm) shell would have a 1m radius (and given how the detDist is calculated, even then that would require specific, ideal circumstances to return that 1m value); + //anything smaller is for all points and purposes going to be impact, so lets just transfer this to that section of the code { - if (shrapnelCount > 0) + if (thickness < (shrapnelThickness * 1.41f)) { - if (thickness < (shrapnelThickness * 1.41f)) - { - //armor breach + //armor breach - volumeToReduce = ((caliber * thickness * (caliber * 4)) / 1000); //cm3 - hitPart.ReduceArmor(volumeToReduce); + volumeToReduce = ((caliber * thickness * (caliber * 4)) / 1000); //cm3 + hitPart.ReduceArmor(volumeToReduce); - if (BDArmorySettings.DRAW_ARMOR_LABELS) - { - Debug.Log("[BDArmory.ProjectileUtils]: Shrapnel penetration on " + hitPart.name + ", " + hitPart.vessel.GetName() + "; " + +shrapnelCount + " hits; Armor damage: " + volumeToReduce + "; part damage: "); - } - damage = hitPart.AddBallisticDamage(shrapnelMass, 0.1f, 1, (shrapnelThickness / thickness), 1, 430, explosionSource); //within 5 calibers shrapnel still getting pushed/accelerated by blast - ApplyScore(hitPart, sourceVesselName, 0, damage, "Shrapnel", explosionSource); - CalculateArmorDamage(hitPart, (shrapnelThickness / thickness), (caliber * 0.4f), hardness, Ductility, Density, 430, sourceVesselName, explosionSource, armorType); - BattleDamageHandler.CheckDamageFX(hitPart, caliber, (shrapnelThickness / thickness), true, false, sourceVesselName, hit); + if (BDArmorySettings.DEBUG_ARMOR) + { + Debug.Log("[BDArmory.ProjectileUtils{CalcShrapnel}]: Shrapnel penetration on " + hitPart.name + ", " + hitPart.vessel.GetName() + "; " + +shrapnelCount + " hits; Armor damage: " + volumeToReduce + "; part damage: "); } - else + damage = hitPart.AddBallisticDamage(shrapnelMass, 0.1f, 1, (shrapnelThickness / thickness), 1, 430, explosionSource); //within 5 calibers shrapnel still getting pushed/accelerated by blast + ApplyScore(hitPart, sourceVesselName, 0, damage, "Shrapnel", explosionSource); + CalculateArmorDamage(hitPart, (shrapnelThickness / thickness), (caliber * 0.4f), hardness, Ductility, Density, 430, sourceVesselName, explosionSource, armorType); + BattleDamageHandler.CheckDamageFX(hitPart, caliber, (shrapnelThickness / thickness), true, false, sourceVesselName, hit); + } + else + { + if (thickness < (shrapnelThickness * 1.7))//armor cracks; { - if (thickness < (shrapnelThickness * 1.7))//armor cracks; + volumeToReduce = ((Mathf.CeilToInt(caliber / 500) * Mathf.CeilToInt(caliber / 500)) * (50 * 50) * ((float)hitPart.GetArmorMaxThickness() / 10)); //cm3 + hitPart.ReduceArmor(volumeToReduce); + if (BDArmorySettings.DEBUG_ARMOR) { - volumeToReduce = ((Mathf.CeilToInt(caliber / 500) * Mathf.CeilToInt(caliber / 500)) * (50 * 50) * ((float)hitPart.GetArmorMaxThickness() / 10)); //cm3 - hitPart.ReduceArmor(volumeToReduce); - if (BDArmorySettings.DRAW_ARMOR_LABELS) - { - Debug.Log("[BDArmory.ProjectileUtils]: Explosive Armor failure; Armor damage: " + volumeToReduce + " on " + hitPart.name + ", " + hitPart.vessel.GetName()); - } + Debug.Log("[BDArmory.ProjectileUtils{CalcShrapnel}]: Explosive Armor failure; Armor damage: " + volumeToReduce + " on " + hitPart.name + ", " + hitPart.vessel.GetName()); } } } } + */ } - else //detonates in armor + else //detonates on/in armor { if (penetrationFactor < 1 && penetrationFactor > 0) { @@ -387,21 +490,33 @@ public static void CalculateShrapnelDamage(Part hitPart, RaycastHit hit, float c volumeToReduce = ((caliber * thickness * (caliber * 4)) * 2) / 1000; //cm3 hitPart.ReduceArmor(volumeToReduce); - if (BDArmorySettings.DRAW_ARMOR_LABELS) + if (BDArmorySettings.DEBUG_ARMOR) { - Debug.Log("[BDArmory.ProjectileUtils]: Shrapnel penetration from in-armor detonation, " + hitPart.name + ", " + hitPart.vessel.GetName() + "; Armor damage: " + volumeToReduce + "; part damage: "); + Debug.Log("[BDArmory.ProjectileUtils{CalcShrapnel}]: Shrapnel penetration from on-armor detonation, " + hitPart.name + ", " + hitPart.vessel.GetName() + "; Armor damage: " + volumeToReduce + "; part damage: "); } - damage = hitPart.AddBallisticDamage((projmass * (1 - HERatio)), 0.1f, 1, (shrapnelThickness / thickness), 1, 430, explosionSource); //within 5 calibers shrapnel still getting pushed/accelerated by blast + damage = hitPart.AddBallisticDamage(((projmass / 2) * (1 - HERatio)), 0.1f, 1, (shrapnelThickness / thickness), 1, 430, explosionSource); //within 5 calibers shrapnel still getting pushed/accelerated by blast ApplyScore(hitPart, sourceVesselName, 0, damage, "Shrapnel", explosionSource); CalculateArmorDamage(hitPart, (shrapnelThickness / thickness), (caliber * 1.4f), hardness, Ductility, Density, 430, sourceVesselName, explosionSource, armorType); BattleDamageHandler.CheckDamageFX(hitPart, caliber, (shrapnelThickness / thickness), true, false, sourceVesselName, hit); } + else + { + if (thickness < (shrapnelThickness * 1.7))//armor cracks; + { + volumeToReduce = ((Mathf.CeilToInt(caliber / 500) * Mathf.CeilToInt(caliber / 500)) * (50 * 50) * ((float)hitPart.GetArmorMaxThickness() / 10)); //cm3 + hitPart.ReduceArmor(volumeToReduce); + if (BDArmorySettings.DEBUG_ARMOR) + { + Debug.Log("[BDArmory.ProjectileUtils{CalcShrapnel}]: Explosive Armor failure; Armor damage: " + volumeToReduce + " on " + hitPart.name + ", " + hitPart.vessel.GetName()); + } + } + } } else //internal detonation { - if (BDArmorySettings.DRAW_ARMOR_LABELS) + if (BDArmorySettings.DEBUG_ARMOR) { - Debug.Log("[BDArmory.ProjectileUtils]: Through-armor detonation in " + hitPart.name + ", " + hitPart.vessel.GetName()); + Debug.Log("[BDArmory.ProjectileUtils{CalcShrapnel}]: Through-armor detonation in " + hitPart.name + ", " + hitPart.vessel.GetName()); } damage = hitPart.AddBallisticDamage((projmass * (1 - HERatio)), 0.1f, 1, 1.9f, 1, 430, explosionSource); //internal det catches entire shrapnel mass ApplyScore(hitPart, sourceVesselName, 0, damage, "Shrapnel", explosionSource); @@ -409,31 +524,37 @@ public static void CalculateShrapnelDamage(Part hitPart, RaycastHit hit, float c } } } - public static bool CalculateExplosiveArmorDamage(Part hitPart, double BlastPressure, string sourcevessel, RaycastHit hit, ExplosionSourceType explosionSource) + public static bool CalculateExplosiveArmorDamage(Part hitPart, double BlastPressure, double distance, string sourcevessel, RaycastHit hit, ExplosionSourceType explosionSource, float radius) { /// /// Calculates if shockwave from detonation is stopped by armor, and if not, how much damage is done to armor and part in case of armor rupture or spalling /// Returns boolean; True = armor stops explosion, False = armor blowthrough /// //use blastTotalPressure to get MPa of shock on plate, compare to armor mat tolerances + if (BDArmorySettings.PAINTBALL_MODE) return true; //don't damage armor if paintball mode float thickness = (float)hitPart.GetArmorThickness(); if (thickness <= 0) return false; //no armor to stop explosion + float armorArea = -1; float spallArea; //using this as a hack for affected srf. area, convert m2 to cm2 float spallMass; float damage; var Armor = hitPart.FindModuleImplementing(); if (Armor != null) { - if (hitPart.name.ToLower().Contains("armor")) + if (IsArmorPart(hitPart)) { - spallArea = hitPart.Modules.GetModule().armorVolume * 10000; + armorArea = hitPart.Modules.GetModule().armorVolume * 10000; + spallArea = Mathf.Min(armorArea, radius * radius * 2500); //clamp based on max size of explosion } else { if (Armor.ArmorTypeNum == 1) return false;//ArmorType "None"; no armor to block/reduce blast, take full damage - spallArea = (!double.IsNaN(hitPart.radiativeArea) ? (float)hitPart.radiativeArea : hitPart.GetArea() / 3) * 10000; + armorArea = !double.IsNaN(hitPart.radiativeArea) ? (float)hitPart.radiativeArea : hitPart.GetArea() * 10000; + spallArea = Mathf.Min(armorArea / 3, radius * radius * 2500); } - if (BDArmorySettings.DRAW_ARMOR_LABELS && double.IsNaN(hitPart.radiativeArea)) + //have this scaled by blowthrough factor? afterall a very powerful blast right next to the plate is more likely to punch a localzied hole rather than generally push the whole plate, no? + if (distance < radius / 3) spallArea /= 4; + if (BDArmorySettings.DEBUG_ARMOR && double.IsNaN(hitPart.radiativeArea)) { Debug.Log("[BDArmory.ProjectileUtils{CalculateExplosiveArmorDamage}]: radiative area of part " + hitPart + " was NaN, using approximate area " + spallArea + " instead."); } @@ -441,27 +562,29 @@ public static bool CalculateExplosiveArmorDamage(Part hitPart, double BlastPress float hardness = Armor.Hardness; float Strength = Armor.Strength; float Density = Armor.Density; - if (Armor.ArmorPanel) spallArea = Armor.armorVolume; - float ArmorTolerance = Strength * (1 + ductility) * (thickness / 10); //FIXME - this is going to return a value an order of magnitude greater than blast - //Trying thickness /10, so the 10x increase in thickness in mm isn't massively increasing value + float ArmorTolerance = (((Strength * (1 + ductility)) + Density) / 1000) * thickness; float blowthroughFactor = (float)BlastPressure / ArmorTolerance; - if (BDArmorySettings.DRAW_ARMOR_LABELS) + if (BDArmorySettings.DEBUG_ARMOR) { - Debug.Log("[BDArmory.ProjectileUtils]: Beginning ExplosiveArmorDamage(); " + hitPart.name + ", ArmorType:" + Armor.ArmorTypeNum + "; Armor Thickness: " + thickness + "; BlastPressure: " + BlastPressure + "; BlowthroughFactor: " + blowthroughFactor); ; + Debug.Log("[BDArmory.ProjectileUtils{CalculateExplosiveArmorDamage}]: Beginning ExplosiveArmorDamage(); " + hitPart.name + ", ArmorType:" + Armor.ArmorTypeNum + "; Armor Thickness: " + thickness + "; BlastPressure: " + BlastPressure + "; BlowthroughFactor: " + blowthroughFactor); ; } //is BlastUtils maxpressure in MPa? confirm blast pressure from ExplosionUtils on same scale/magnitude as armorTolerance //something is going on, 25mm steed is enough to no-sell Hellfires (13kg tnt, 33m blastRadius + + //FIXME - something is still not working correctly, and return lesser and lesser damage as armor is reduced, should be otherway around. + //Armor sundering doesn't seem to be working, get debug numbers for mass/area + if (ductility > 0.20f) { - if (BlastPressure > ArmorTolerance) //material stress tolerance exceeded, armor rupture + if (BlastPressure >= ArmorTolerance) //material stress tolerance exceeded, armor rupture { spallMass = spallArea * (thickness / 10) * (Density / 1000000); //entirety of armor lost hitPart.ReduceArmor(spallArea * thickness / 10); //cm3 - if (BDArmorySettings.DRAW_ARMOR_LABELS) + if (BDArmorySettings.DEBUG_ARMOR) { - Debug.Log("[BDArmory.ProjectileUtils]: Armor rupture on " + hitPart.name + ", " + hitPart.vessel.GetName() + "! Size: " + spallArea + "; mass: " + spallMass + "kg"); + Debug.Log("[BDArmory.ProjectileUtils{CalculateExplosiveArmorDamage}]: Armor rupture on " + hitPart.name + ", " + hitPart.vessel.GetName() + "! Size: " + spallArea + "; mass: " + spallMass + "kg"); } damage = hitPart.AddBallisticDamage(spallMass / 1000, spallArea, 1, blowthroughFactor, 1, 422.75f, explosionSource); ApplyScore(hitPart, sourcevessel, 0, damage, "Spalling", explosionSource); @@ -476,33 +599,36 @@ public static bool CalculateExplosiveArmorDamage(Part hitPart, double BlastPress if (blowthroughFactor > 0.66) { spallArea *= ((1 - ductility) * blowthroughFactor); - spallMass = spallArea * (thickness / 30) * (Density / 1000000); //lose 1/3rd thickness from spalling - if (BDArmorySettings.DRAW_ARMOR_LABELS) + + spallMass = Mathf.Min(spallArea, armorArea) * ((thickness / 10) * (blowthroughFactor - 0.66f)) * (Density / 1000000); //lose up to 1/3rd thickness from spalling, based on severity of blast + if (spallArea > armorArea) spallArea = armorArea; + if (BDArmorySettings.DEBUG_ARMOR) { - Debug.Log("[BDArmory.ProjectileUtils]: Explosive Armor spalling" + hitPart.name + ", " + hitPart.vessel.GetName() + "! Size: " + spallArea + "; mass: " + spallMass + "kg"); + Debug.Log("[BDArmory.ProjectileUtils{CalculateExplosiveArmorDamage}]: Explosive Armor spalling" + hitPart.name + ", " + hitPart.vessel.GetName() + "! Size: " + spallArea + "; mass: " + spallMass + "kg"); } if (hardness > 500)//armor holds, but spalling { damage = hitPart.AddBallisticDamage(spallMass / 1000, spallArea, 1, blowthroughFactor, 1, 422.75f, explosionSource); ApplyScore(hitPart, sourcevessel, 0, damage, "Spalling", explosionSource); } - //else soft enough to not spall. Armor has suffered some deformation, though, weakening it. - hitPart.ReduceArmor(spallArea * (thickness / 30)); //cm3 + //else soft enough to not spall. Armor has suffered some deformation, though, weakening it.= if (BDArmorySettings.BATTLEDAMAGE) { BattleDamageHandler.CheckDamageFX(hitPart, spallArea, blowthroughFactor, false, false, sourcevessel, hit); } + spallArea *= (thickness / 10) * (blowthroughFactor - 0.66f); + hitPart.ReduceArmor(spallArea); //cm3 return true; } } else //ductility < 0.20 { - if (blowthroughFactor > 1) + if (blowthroughFactor >= 1) { if (ductility < 0.05f) //ceramics { var volumeToReduce = (Mathf.CeilToInt(spallArea / 500) * Mathf.CeilToInt(spallArea / 500)) * (50 * 50) * ((float)hitPart.GetArmorMaxThickness() / 10); //cm3 - //total failue of 50x50cm armor tile(s) + //total failue of 50x50cm armor tile(s) if (hardness > 500) { spallMass = volumeToReduce * (Density / 1000000); @@ -512,9 +638,9 @@ public static bool CalculateExplosiveArmorDamage(Part hitPart, double BlastPress //soft stuff like Aramid not likely to cause major damage hitPart.ReduceArmor(volumeToReduce); //cm3 - if (BDArmorySettings.DRAW_ARMOR_LABELS) + if (BDArmorySettings.DEBUG_ARMOR) { - Debug.Log("[BDArmory.ProjectileUtils]: Armor destruction on " + hitPart.name + ", " + hitPart.vessel.GetName() + "!"); + Debug.Log("[BDArmory.ProjectileUtils{CalculateExplosiveArmorDamage}]: Armor destruction on " + hitPart.name + ", " + hitPart.vessel.GetName() + "!"); } if (BDArmorySettings.BATTLEDAMAGE) { @@ -523,15 +649,16 @@ public static bool CalculateExplosiveArmorDamage(Part hitPart, double BlastPress } else //0.05-0.19 ductility - harder steels, etc { - spallArea *= ((1.2f - ductility) * blowthroughFactor); - spallMass = spallArea * thickness / 10 * (Density / 1000000); - hitPart.ReduceArmor(spallArea * (thickness / 10)); //cm3 - damage = hitPart.AddBallisticDamage(spallMass / 1000, spallArea * 10, 1, blowthroughFactor, 1, 422.75f, explosionSource); + spallArea *= ((1.2f - ductility) * blowthroughFactor * (thickness / 10)); + if (spallArea > armorArea) spallArea = armorArea; + spallMass = spallArea * (Density / 1000000); + hitPart.ReduceArmor(spallArea); //cm3 + damage = hitPart.AddBallisticDamage(spallMass / 1000, spallArea / 100000, 1, blowthroughFactor, 1, 422.75f, explosionSource); ApplyScore(hitPart, sourcevessel, 0, damage, "Spalling", explosionSource); - if (BDArmorySettings.DRAW_ARMOR_LABELS) + if (BDArmorySettings.DEBUG_ARMOR) { - Debug.Log("[BDArmory.ProjectileUtils]: Armor sundered, " + hitPart.name + ", " + hitPart.vessel.GetName() + "!"); + Debug.Log("[BDArmory.ProjectileUtils{CalculateExplosiveArmorDamage}]: Armor sundered, " + hitPart.name + ", " + hitPart.vessel.GetName() + "!"); } if (BDArmorySettings.BATTLEDAMAGE) { @@ -554,8 +681,8 @@ public static bool CalculateExplosiveArmorDamage(Part hitPart, double BlastPress { if (ductility < 0.05f) { - var volumeToReduce = (Mathf.CeilToInt(spallArea / 500) * Mathf.CeilToInt(spallArea / 500)) * (50 * 50) * ((float)hitPart.GetArmorMaxThickness() / 10); //cm3 - //total failue of 50x50cm armor tile(s) + var volumeToReduce = Mathf.CeilToInt(spallArea / 2500) * (50 * 50) * ((float)hitPart.GetArmorMaxThickness() / 10); //cm3 + //total failue of 50x50cm armor tile(s) if (hardness > 500) { spallMass = volumeToReduce * (Density / 1000000); @@ -565,9 +692,9 @@ public static bool CalculateExplosiveArmorDamage(Part hitPart, double BlastPress //soft stuff like Aramid not likely to cause major damage hitPart.ReduceArmor(volumeToReduce); //cm3 - if (BDArmorySettings.DRAW_ARMOR_LABELS) + if (BDArmorySettings.DEBUG_ARMOR) { - Debug.Log("[BDArmory.ProjectileUtils]: Armor destruction on " + hitPart.name + ", " + hitPart.vessel.GetName() + "!"); + Debug.Log("[BDArmory.ProjectileUtils{CalculateExplosiveArmorDamage}]: Armor destruction on " + hitPart.name + ", " + hitPart.vessel.GetName() + "!"); } if (BDArmorySettings.BATTLEDAMAGE) { @@ -576,22 +703,24 @@ public static bool CalculateExplosiveArmorDamage(Part hitPart, double BlastPress } else //0.05-0.19 ductility - harder steels, etc { - spallArea *= ((1.2f - ductility) * blowthroughFactor); + spallArea *= ((1.2f - ductility) * blowthroughFactor) * ((thickness / 10) * (blowthroughFactor - 0.66f)); + if (spallArea > armorArea) spallArea = armorArea; if (hardness > 500) { - spallMass = spallArea * thickness / 30 * (Density / 1000000); - damage = hitPart.AddBallisticDamage(spallMass / 1000, spallArea * 10, 1, blowthroughFactor, 1, 422.75f, explosionSource); + //blowtrhoughFactor - 1 * 100 + spallMass = spallArea * (Density / 1000000); + damage = hitPart.AddBallisticDamage(spallMass / 1000, spallArea / 100000, 1, blowthroughFactor, 1, 422.75f, explosionSource); ApplyScore(hitPart, sourcevessel, 0, damage, "Spalling", explosionSource); } - hitPart.ReduceArmor(spallArea * (thickness / 30)); //cm3 + hitPart.ReduceArmor(spallArea); //cm3 - if (BDArmorySettings.DRAW_ARMOR_LABELS) + if (BDArmorySettings.DEBUG_ARMOR) { - Debug.Log("[BDArmory.ProjectileUtils]: Armor sundered, " + hitPart.name + ", " + hitPart.vessel.GetName() + "!"); + Debug.Log("[BDArmory.ProjectileUtils{CalculateExplosiveArmorDamage}]: Armor holding. Barely!, " + hitPart.name + ", " + hitPart.vessel.GetName() + "!; area lost: " + spallArea + "cm3; mass: " + spallArea * (Density / 1000000) + "kg"); } if (BDArmorySettings.BATTLEDAMAGE) { - BattleDamageHandler.CheckDamageFX(hitPart, spallArea, blowthroughFactor, true, false, sourcevessel, hit); + BattleDamageHandler.CheckDamageFX(hitPart, spallArea / 100000, blowthroughFactor, true, false, sourcevessel, hit); } } return true; @@ -621,7 +750,7 @@ public static float CalculatePenetration(float caliber, float projMass, float im public static float CalculateProjectileEnergy(float projMass, float impactVel) { float bulletEnergy = (projMass * 1000) * impactVel; //(should this be 1/2(mv^2) instead? prolly at somepoint, but the abstracted calcs I have use mass x vel, and work, changing it would require refactoring calcs - if (BDArmorySettings.DRAW_ARMOR_LABELS) + if (BDArmorySettings.DEBUG_ARMOR) { Debug.Log("[BDArmory.ProjectileUtils]: Bullet Energy: " + bulletEnergy + "; mass: " + projMass + "; vel: " + impactVel); } @@ -639,7 +768,7 @@ public static float CalculateArmorStrength(float caliber, float thickness, float //initial impact calc //determine yieldstrength of material float yieldStrength; - if (BDArmorySettings.DRAW_ARMOR_LABELS) + if (BDArmorySettings.DEBUG_ARMOR) { Debug.Log("[BDArmory.ProjectileUtils]: properties: Tensile:" + Strength + "; Ductility: " + Ductility + "; density: " + Density + "; thickness: " + thickness + "; caliber: " + caliber); } @@ -667,7 +796,7 @@ public static float CalculateArmorStrength(float caliber, float thickness, float yieldStrength *= 0.5f; } } - if (BDArmorySettings.DRAW_ARMOR_LABELS) + if (BDArmorySettings.DEBUG_ARMOR) { Debug.Log("[BDArmory.ProjectileUtils]: Armor yield Strength: " + yieldStrength); } @@ -675,7 +804,7 @@ public static float CalculateArmorStrength(float caliber, float thickness, float return yieldStrength; } - public static float CalculateDeformation(float yieldStrength, float bulletEnergy, float caliber, float impactVel, float hardness, float Density, float HEratio, float apBulletMod) + public static float CalculateDeformation(float yieldStrength, float bulletEnergy, float caliber, float impactVel, float hardness, float Density, float HEratio, float apBulletMod, bool sabot) { if (bulletEnergy < yieldStrength) return caliber; //armor stops the round, but calc armor damage else //bullet penetrates. Calculate what happens to the bullet @@ -683,24 +812,26 @@ public static float CalculateDeformation(float yieldStrength, float bulletEnergy //deform bullet from impact if (yieldStrength < 1) yieldStrength = 1000; float BulletDurabilityMod = ((1 - HEratio) * (caliber / 25)); //rounds that are larger, or have less HE, are structurally stronger and betterresist deformation. Add in a hardness factor for sabots/DU rounds? - if (BDArmorySettings.DRAW_ARMOR_LABELS) + if (BDArmorySettings.DEBUG_ARMOR) { Debug.Log("[BDArmory.ProjectileUtils{Calc Deformation}]: yield:" + yieldStrength + "; Energy: " + bulletEnergy + "; caliber: " + caliber + "; impactVel: " + impactVel); Debug.Log("[BDArmory.ProjectileUtils{Calc Deformation}]: hardness:" + hardness + "; BulletDurabilityMod: " + BulletDurabilityMod + "; density: " + Density); } - float newCaliber = ((((yieldStrength / bulletEnergy) * (hardness * Mathf.Sqrt(Density / 1000))) / impactVel) / (BulletDurabilityMod * apBulletMod)); //faster penetrating rounds less deformed, thin armor will impart less deformation before failing - if (impactVel > 1250) //too fast and steel/lead begin to melt on impact - hence DU/Tungsten hypervelocity penetrators + float newCaliber = ((((yieldStrength / bulletEnergy) * (hardness * BDAMath.Sqrt(Density / 1000))) / impactVel) / (BulletDurabilityMod * apBulletMod)); //faster penetrating rounds less deformed, thin armor will impart less deformation before failing + if (!sabot && impactVel > 1250) //too fast and steel/lead begin to melt on impact - hence DU/Tungsten hypervelocity penetrators { newCaliber *= (impactVel / 1250); } newCaliber = Mathf.Clamp(newCaliber, 1f, 5f); - //replace this with tensile srength of bullet calcs? - if (BDArmorySettings.DRAW_ARMOR_LABELS) + //replace this with tensile srength of bullet calcs? - really should, else a 30m/s impact is capable of deforming a bullet... + //float bulletStrength = caliber * caliber * Mathf.PI / 400f * 840 * (11.34f) * caliber * 3; //how would this work - if bulletStrength is greater than yieldstrength, don't deform? + + if (BDArmorySettings.DEBUG_ARMOR) { Debug.Log("[BDArmory.ProjectileUtils{Calc Deformation}]: Bullet Deformation modifier " + newCaliber); } newCaliber *= caliber; - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.ProjectileUtils{Calc Deformation}]: bullet now " + (newCaliber) + " mm"); + if (BDArmorySettings.DEBUG_ARMOR) Debug.Log("[BDArmory.ProjectileUtils{Calc Deformation}]: bullet now " + (newCaliber) + " mm"); return newCaliber; } } @@ -711,13 +842,13 @@ public static bool CalculateBulletStatus(float projMass, float newCaliber, bool float density = 11.34f; if (sabot) { - density = 19; + density = 19.1f; } float bulletLength = ((projMass * 1000) / ((newCaliber * newCaliber * Mathf.PI / 400) * density) + 1) * 10; //srf.Area in mmm2 x density of lead to get mass per 1 cm length of bullet / total mass to get total length, //+ 10 to accound for ogive/mushroom head post-deformation instead of perfect cylinder if (newCaliber > (bulletLength * 2)) //has the bullet flattened into a disc, and is no longer a viable penetrator? { - if (BDArmorySettings.DRAW_ARMOR_LABELS) + if (BDArmorySettings.DEBUG_ARMOR) { Debug.Log("[BDArmory.ProjectileUtils]: Bullet deformed past usable limit"); } @@ -726,7 +857,95 @@ public static bool CalculateBulletStatus(float projMass, float newCaliber, bool else return true; } - public static float CalculatePenetration(float caliber, float newCaliber, float projMass, float impactVel, float Ductility, float Density, float Strength, float thickness, float APmod, bool sabot = false) + + public static float CalculatePenetration(float caliber, float bulletVelocity, + float bulletMass, float apBulletMod, float Strength = 940, float vFactor = 0.00000094776185184f, + float muParam1 = 0.656060636f, float muParam2 = 1.20190930f, float muParam3 = 1.77791929f, bool sabot = false, + float length = 0) + { + // Calculate the length of the projectile + if (length == 0) + { + length = ((bulletMass * 1000.0f * 400.0f) / ((caliber * caliber * + Mathf.PI) * (sabot ? 19.0f : 11.34f)) + 1.0f) * 10.0f; + } + + //float penetration = 0; + // 1400 is an arbitrary velocity around where the linear function used to + // simplify diverges from the result predicted by the Frank and Zook S2 based + // equation used. It is also inaccurate under 1400 for long rod projectiles + // with AR > 4, however I'm using 6 because it's still more or less OK at that + // point and we may as well try to cover more projectiles with the super + // performant formula. Any projectiles with AR < 1 are also going to use the + // performant formula because the model used is for long rods primarily and + // at AR < 1 the penetration starts climbing again which doesn't make sense to + // me physically + + // Old restrictions on when to use IDA equation + /* + if (((bulletVelocity < 1400) && (length > 6 * caliber)) || + (length < caliber)) + */ + // New restriction is only to do so if the L/D ratio is < 1 where Tate starts + // overpredicting the penetration values significantly. This is bad if there's + // any hypervelocity rounds with L/D < 1 or hypervelocity rounds with L/D < 4 + // that are not deformed enough after impact to still be valid for another + // impact and have L/D < 1 at that point since if they're at super high + // velocities the linear nature of this equation will overpredict penetration + // Perhaps capping this with the hydrodynamic limit makes sense, but even with + // these kind of penetrators they easily blow past the hydrodynamic limit in + // actual experiments so I'm a little hesitant about putting it in. + + // Above text has been deprecated, Tate is used for everything and projectile + // aspect ratio is now used to reduce penetration at L/D < 1 + + float penetration = ((length - caliber) * (1.0f - Mathf.Exp((-vFactor * + bulletVelocity * bulletVelocity) * muParam1)) * muParam2 + caliber * + muParam3 * Mathf.Log(1.0f + vFactor * bulletVelocity * + bulletVelocity)) * apBulletMod; + + if (length < caliber) + { + // Formula based on IDA paper P5032, Appendix D, modified to match the + // Krupp equation this mod used before. + //penetration = (BDAMath.Sqrt(bulletMass * 1000.0f / (0.7f * Strength * Mathf.PI + // * caliber)) * 0.727457902089f * bulletVelocity) * apBulletMod; + + // Deprecated the above formula in favor of this, it actually follows the + // old Krupp formula's predictions pretty well. It may not necessarily be + // 100% accurate but it gets the job done + penetration = penetration * length / caliber; + } + /*else + { + // Formula based on "Energy-efficient penetration and perforation of + // targets in the hypervelocity regime" by Frank and Zook (1987) Used the + // S2 model for homogenous targets where Y = H which is a bad assumption + // and is an overestimate but the S4 option is far more complex than even + // this and it also requires an empirical parameter that requires testing + // long rod penetrators against targets so lolno + penetration = ((length - caliber) * (1.0f - Mathf.Exp((-vFactor * + bulletVelocity * bulletVelocity) * muParam1)) * muParam2 + caliber * + muParam3 * Mathf.Log(1.0f + vFactor * bulletVelocity * + bulletVelocity)) * apBulletMod; + }*/ + + + if (BDArmorySettings.DEBUG_ARMOR) + { + Debug.Log("[BDArmory.ProjectileUtils{Calc Penetration}]: Caliber: " + caliber + " Length: " + length + "; sabot: " + sabot + " ;Penetration: " + Mathf.Round(penetration / 10) + " cm"); + Debug.Log("[BDArmory.ProjectileUtils{Calc Penetration}]: vFactor: " + vFactor + "; EXP: " + Mathf.Exp((-vFactor * + bulletVelocity * bulletVelocity) * muParam1) + " ;MuParam1: " + muParam1); + Debug.Log("[BDArmory.ProjectileUtils{Calc Penetration}]: MuParam2: " + muParam2 + "; muParam3: " + muParam3 + " ;log: " + Mathf.Log(1.0f + vFactor * bulletVelocity * + bulletVelocity)); + } + return penetration; + } + + /* + // Deprecated formula + // Using this for the moment as the Tate formula doesn't work well with ceramic/ceramic-adjacent ultra-low ductility armor materials. Numbers aren't as accurate, but are close enough for BDA + public static float CalculateCeramicPenetration(float caliber, float newCaliber, float projMass, float impactVel, float Ductility, float Density, float Strength, float thickness, float APmod, bool sabot = false) { float Energy = CalculateProjectileEnergy(projMass, impactVel); if (thickness < 1) @@ -737,37 +956,40 @@ public static float CalculatePenetration(float caliber, float newCaliber, float float penetration; //bullet's deformed, penetration using larger crosssection + //caliber in mm, converted to length in cm, converted to mm + float length = ((projMass * 1000) / ((newCaliber * newCaliber * Mathf.PI / 400) * (sabot ? 19.1f : 11.34f)) + 1) * 10; + //if (impactVel > 1500) + //penetration = length * BDAMath.Sqrt((sabot ? 19100 : 11340) / Density); //at hypervelocity, impacts are akin to fluid displacement + //penetration in mm + //sabots should have a caliber check, or a mass check? - else a lighter, smaller caliber sabot of equal length will have similar penetration charateristics as a larger, heavier round..? + //or just have sabots that are too narrow simply snap due to structural stress... + var modifiedCaliber = (0.5f * caliber) + (0.5f * newCaliber) * (2f * Ductility * Ductility); + float yieldStrength = modifiedCaliber * modifiedCaliber * Mathf.PI / 100f * Strength * (Density / 7850f) * thickness; + if (Ductility > 0.25f) //up to a point, anyway. Stretch too much... { - //caliber in mm, converted to length in cm, converted to mm - float length = ((projMass * 1000) / ((newCaliber * newCaliber * Mathf.PI / 400) * (sabot ? 19 : 11.34f)) + 1) * 10; - //if (impactVel > 1500) - //penetration = length * Mathf.Sqrt((sabot ? 19000 : 11340) / Density); //at hypervelocity, impacts are akin to fluid displacement - //penetration in mm - - var modifiedCaliber = (0.5f * caliber) + (0.5f * newCaliber) * (2f * Ductility * Ductility); - float yieldStrength = modifiedCaliber * modifiedCaliber * Mathf.PI / 100f * Strength * (Density / 7850f) * thickness; - if (Ductility > 0.25f) //up to a point, anyway. Stretch too much... - { - yieldStrength *= 0.7f; //necking and point embrittlement reduce total tensile strength of material - } - penetration = Mathf.Min(((Energy / yieldStrength) * thickness * APmod), length * Mathf.Sqrt((sabot ? 19000 : 11340) / Density)); - //cap penetration to max possible pen depth from hypervelocity impact - } //penetration in mm + yieldStrength *= 0.7f; //necking and point embrittlement reduce total tensile strength of material + } + penetration = Mathf.Min(((Energy / yieldStrength) * thickness * APmod), (length * BDAMath.Sqrt((sabot ? 19100 : 11340) / Density) * (sabot ? 0.385f : 1) * APmod)); + //cap penetration to max possible pen depth from hypervelocity impact + //need to re-add APBulletMod to sabots, also need to reduce sabot pen depth by about 0.6x; Abrams sabot ammos can apparently pen about their length through steel + //penetration in mm //apparently shattered projectiles add 30% to armor thickness; oblique impact beyond 55deg decreases effective thickness(splatted projectile digs in to plate instead of richochets) - if (BDArmorySettings.DRAW_ARMOR_LABELS) + if (BDArmorySettings.DEBUG_ARMOR) { Debug.Log("[BDArmory.ProjectileUtils{Calc Penetration}]: Energy: " + Energy + "; caliber: " + caliber + "; newCaliber: " + newCaliber); Debug.Log("[BDArmory.ProjectileUtils{Calc Penetration}]: Ductility:" + Ductility + "; Density: " + Density + "; Strength: " + Strength + "; thickness: " + thickness); - Debug.Log("[BDArmory.ProjectileUtils{Calc Penetration}]: Penetration: " + Mathf.Round(penetration / 10) + " cm"); + Debug.Log("[BDArmory.ProjectileUtils{Calc Penetration}]: Length: " + length + "; sabot: " + sabot + " ;Penetration: " + Mathf.Round(penetration / 10) + " cm"); } return penetration; } + */ public static float CalculateThickness(Part hitPart, float anglemultiplier) { float thickness = (float)hitPart.GetArmorThickness(); //return mm - return Mathf.Max(thickness / anglemultiplier, 1); + // return Mathf.Max(thickness / (anglemultiplier > 0.001f ? anglemultiplier : 0.001f), 1); + return Mathf.Max(thickness / Mathf.Abs(anglemultiplier), 1); } public static bool CheckGroundHit(Part hitPart, RaycastHit hit, float caliber) { @@ -788,8 +1010,8 @@ public static bool CheckBuildingHit(RaycastHit hit, float projMass, Vector3 curr try { building = hit.collider.gameObject.GetComponentUpwards(); - if (building != null) - building.damageDecay = 600f; + //if (building != null) + // building.damageDecay = 600f; //check if new method is still subject to building regen } catch (Exception e) { @@ -798,19 +1020,23 @@ public static bool CheckBuildingHit(RaycastHit hit, float projMass, Vector3 curr if (building != null && building.IsIntact) { + if (BDArmorySettings.BUILDING_DMG_MULTIPLIER == 0) return true; float damageToBuilding = ((0.5f * (projMass * (currentVelocity.magnitude * currentVelocity.magnitude))) - * (BDArmorySettings.DMG_MULTIPLIER / 100) * DmgMult + * (BDArmorySettings.DMG_MULTIPLIER / 100) * DmgMult * BDArmorySettings.BALLISTIC_DMG_FACTOR * 1e-4f); damageToBuilding /= 8f; - building.AddDamage(damageToBuilding); - if (building.Damage > building.impactMomentumThreshold * 150) + damageToBuilding *= BDArmorySettings.BUILDING_DMG_MULTIPLIER; + BuildingDamage.RegisterDamage(building); + building.FacilityDamageFraction += damageToBuilding; + if (building.FacilityDamageFraction > (building.impactMomentumThreshold * 2)) { + if (BDArmorySettings.DEBUG_DAMAGE) Debug.Log("[BDArmory.ProjectileUtils]: Building demolished due to ballistic damage! Dmg to building: " + building.Damage); building.Demolish(); } - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log("[BDArmory.ProjectileUtils]: Ballistic hit destructible building! Hitpoints Applied: " + Mathf.Round(damageToBuilding) + - ", Building Damage : " + Mathf.Round(building.Damage) + - " Building Threshold : " + building.impactMomentumThreshold); + if (BDArmorySettings.DEBUG_DAMAGE) + Debug.Log("[BDArmory.ProjectileUtils]: Ballistic hit destructible building " + building.name + "! Hitpoints Applied: " + damageToBuilding.ToString("F3") + + ", Building Damage : " + building.FacilityDamageFraction + + " Building Threshold : " + building.impactMomentumThreshold * 2); return true; } @@ -859,17 +1085,17 @@ public static float CalculateExplosionProbability(Part part) if (fuelPct > 0 && fuelPct <= 0.60f) { - probability = Core.Utils.BDAMath.RangedProbability(new[] { 50f, 25f, 20f, 5f }); + probability = BDAMath.RangedProbability(new[] { 50f, 25f, 20f, 5f }); } else { - probability = Core.Utils.BDAMath.RangedProbability(new[] { 50f, 25f, 20f, 2f }); + probability = Utils.BDAMath.RangedProbability(new[] { 50f, 25f, 20f, 2f }); } if (fuelPct == 1f || fuelPct == 0f) probability = 0f; - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_WEAPONS) { Debug.Log("[BDArmory.ProjectileUtils]: Explosive Probablitliy " + probability); } @@ -896,7 +1122,7 @@ public static float CalculateExplosionProbability(Part part) } } - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_WEAPONS) { Debug.Log("[BDArmory.ProjectileUtils]: Penetration of bullet detonated fuel!"); } @@ -908,175 +1134,5 @@ public static float CalculateExplosionProbability(Part part) PartExploderSystem.AddPartToExplode(part); } - - private class PriorityQueue - { - private Dictionary> partResources = new Dictionary>(); - - public PriorityQueue(HashSet elements) - { - foreach (PartResource r in elements) - { - Add(r); - } - } - - public void Add(PartResource r) - { - int key = r.part.resourcePriorityOffset; - if (partResources.ContainsKey(key)) - { - List existing = partResources[key]; - existing.Add(r); - partResources[key] = existing; - } - else - { - List newList = new List(); - newList.Add(r); - partResources.Add(key, newList); - } - } - - public List Pop() - { - if (partResources.Count == 0) - { - return new List(); - } - int key = partResources.Keys.Max(); - List result = partResources[key]; - partResources.Remove(key); - return result; - } - - public bool HasNext() - { - return partResources.Count != 0; - } - } - - private static void StealResource(Vessel src, Vessel dst, HashSet resourceNames, double ration, bool integerAmounts = false) - { - if (src == null || dst == null) return; - - // identify all parts on source vessel with resource - Dictionary> srcParts = new Dictionary>(); - DeepFind(src.rootPart, resourceNames, srcParts, BDArmorySettings.RESOURCE_STEAL_RESPECT_FLOWSTATE_OUT); - - // identify all parts on destination vessel with resource - Dictionary> dstParts = new Dictionary>(); - DeepFind(dst.rootPart, resourceNames, dstParts, BDArmorySettings.RESOURCE_STEAL_RESPECT_FLOWSTATE_IN); - - foreach (var resourceName in resourceNames) - { - if (!srcParts.ContainsKey(resourceName) || !dstParts.ContainsKey(resourceName)) - { - // if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log(string.Format("[BDArmory.ProjectileUtils]: Steal resource {0} failed; no parts.", resourceName)); - continue; - } - - double remainingAmount = srcParts[resourceName].Sum(p => p.amount); - if (integerAmounts) - { - remainingAmount = Math.Floor(remainingAmount); - if (remainingAmount == 0) continue; // Nothing left to steal. - } - double amount = remainingAmount * ration; - if (integerAmounts) { amount = Math.Ceiling(amount); } // Round up steal amount so that something is always stolen if there's something to steal. - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.ProjectileUtils]: " + dst.vesselName + " is trying to steal " + amount.ToString("F1") + " of " + resourceName + " from " + src.vesselName); - - // transfer resource from src->dst parts, honoring their priorities - PriorityQueue sourceQueue = new PriorityQueue(srcParts[resourceName]); - PriorityQueue destinationQueue = new PriorityQueue(dstParts[resourceName]); - List sources = null, destinations = null; - double tolerance = 1e-3; - double amountTaken = 0; - while (amount - amountTaken >= (integerAmounts ? 1d : tolerance)) - { - if (sources == null) - { - sources = sourceQueue.Pop(); - if (sources.Count() == 0) break; - } - if (destinations == null) - { - destinations = destinationQueue.Pop(); - if (destinations.Count() == 0) break; - } - var availability = sources.Where(e => e.amount >= tolerance / sources.Count()); // All source parts with something in. - var opportunity = destinations.Where(e => e.maxAmount - e.amount >= tolerance / destinations.Count()); // All destination parts with room to spare. - if (availability.Count() == 0) { sources = null; } - if (opportunity.Count() == 0) { destinations = null; } - if (sources == null || destinations == null) continue; - if (integerAmounts) - { - if (availability.Sum(e => e.amount) < 1d) { sources = null; } - if (opportunity.Sum(e => e.maxAmount - e.amount) < 1d) { destinations = null; } - if (sources == null || destinations == null) continue; - } - var minFractionAvailable = availability.Min(r => r.amount / r.maxAmount); // Minimum fraction of container size available for transfer. - var minFractionOpportunity = opportunity.Min(r => (r.maxAmount - r.amount) / r.maxAmount); // Minimum fraction of container size available to fill a part. - var totalTransferAvailable = availability.Sum(r => r.maxAmount * minFractionAvailable); - var totalTransferOpportunity = opportunity.Sum(r => r.maxAmount * minFractionOpportunity); - var totalTransfer = Math.Min(amount, Math.Min(totalTransferAvailable, totalTransferOpportunity)); // Total amount to transfer that either transfers the amount required, empties a container or fills a container. - if (integerAmounts) { totalTransfer = Math.Floor(totalTransfer); } - var totalContainerSizeAvailable = availability.Sum(r => r.maxAmount); - var totalContainerSizeOpportunity = opportunity.Sum(r => r.maxAmount); - var transferFractionAvailable = totalTransfer / totalContainerSizeAvailable; - var transferFractionOpportunity = totalTransfer / totalContainerSizeOpportunity; - - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log($"[BDArmory.ProjectileUtils]: Transferring {totalTransfer:F1} of {resourceName} from {string.Join(", ", availability.Select(a => $"{a.part.name} ({a.amount:F1}/{a.maxAmount:F1})").ToList())} on {src.vesselName} to {string.Join(", ", opportunity.Select(o => $"{o.part.name} ({o.amount:F1}/{o.maxAmount:F1})").ToList())} on {dst.vesselName}"); - // Transfer directly between parts doesn't seem to be working properly (it leaves the source, but doesn't arrive at the destination). - var measuredOut = 0d; - var measuredIn = 0d; - foreach (var sourceResource in availability) - { measuredOut += sourceResource.part.TransferResource(sourceResource.info.id, -transferFractionAvailable * sourceResource.maxAmount); } - foreach (var destinationResource in opportunity) - { measuredIn += -destinationResource.part.TransferResource(destinationResource.info.id, transferFractionOpportunity * destinationResource.maxAmount); } - if (Math.Abs(measuredIn - measuredOut) > tolerance) - { Debug.LogWarning($"[BDArmory.ProjectileUtils]: Discrepancy in the amount of {resourceName} transferred from {string.Join(", ", availability.Select(r => r.part.name))} ({measuredOut:F3}) to {string.Join(", ", opportunity.Select(r => r.part.name))} ({measuredIn:F3})"); } - - amountTaken += totalTransfer; - if (totalTransfer < tolerance) - { - Debug.LogWarning($"[BDArmory.ProjectileUtils]: totalTransfer was {totalTransfer} for resource {resourceName}, amount: {amount}, availability: {string.Join(", ", availability.Select(r => r.amount))}, opportunity: {string.Join(", ", opportunity.Select(r => r.maxAmount - r.amount))}"); - if (availability.Sum(r => r.amount) < opportunity.Sum(r => r.maxAmount - r.amount)) { sources = null; } else { destinations = null; } - } - } - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log($"[BDArmory.ProjectileUtils]: Final amount of {resourceName} stolen: {amountTaken:F1}"); - } - } - - private class ResourceAllocation - { - public PartResource sourceResource; - public Part destPart; - public double amount; - public ResourceAllocation(PartResource r, Part p, double a) - { - this.sourceResource = r; - this.destPart = p; - this.amount = a; - } - } - - private static void DeepFind(Part p, HashSet resourceNames, Dictionary> accumulator, bool respectFlowState) - { - foreach (PartResource r in p.Resources) - { - if (resourceNames.Contains(r.resourceName)) - { - if (respectFlowState && !r.flowState) continue; // Ignore locked resources. - if (!accumulator.ContainsKey(r.resourceName)) - accumulator[r.resourceName] = new HashSet(); - accumulator[r.resourceName].Add(r); - } - } - foreach (Part child in p.children) - { - DeepFind(child, resourceNames, accumulator, respectFlowState); - } - } } } diff --git a/BDArmory/Utils/ResourceUtils.cs b/BDArmory/Utils/ResourceUtils.cs new file mode 100644 index 000000000..69c3fe1e5 --- /dev/null +++ b/BDArmory/Utils/ResourceUtils.cs @@ -0,0 +1,248 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +using BDArmory.Settings; + +namespace BDArmory.Utils +{ + public static class ResourceUtils + { + public static HashSet FuelResources + { + get + { + if (_FuelResources == null) + { + _FuelResources = new HashSet(); + foreach (var resource in PartResourceLibrary.Instance.resourceDefinitions) + { + if (resource.name.EndsWith("Fuel") || resource.name.EndsWith("Oxidizer") || resource.name.EndsWith("Air") || resource.name.EndsWith("Charge") || resource.name.EndsWith("Gas") || resource.name.EndsWith("Propellant")) // FIXME These ought to be configurable + { _FuelResources.Add(resource.name); } + } + Debug.Log("[BDArmory.ProjectileUtils]: Fuel resources: " + string.Join(", ", _FuelResources)); + } + return _FuelResources; + } + } + static HashSet _FuelResources; + public static HashSet AmmoResources + { + get + { + if (_AmmoResources == null) + { + _AmmoResources = new HashSet(); + foreach (var resource in PartResourceLibrary.Instance.resourceDefinitions) + { + if (resource.name.EndsWith("Ammo") || resource.name.EndsWith("Shell") || resource.name.EndsWith("Shells") || resource.name.EndsWith("Rocket") || resource.name.EndsWith("Rockets") || resource.name.EndsWith("Bolt") || resource.name.EndsWith("Mauser")) + { _AmmoResources.Add(resource.name); } + } + Debug.Log("[BDArmory.ProjectileUtils]: Ammo resources: " + string.Join(", ", _AmmoResources)); + } + return _AmmoResources; + } + } + static HashSet _AmmoResources; + public static HashSet CMResources + { + get + { + if (_CMResources == null) + { + _CMResources = new HashSet(); + foreach (var resource in PartResourceLibrary.Instance.resourceDefinitions) + { + if (resource.name.EndsWith("Flare") || resource.name.EndsWith("Smoke") || resource.name.EndsWith("Chaff")) + { _CMResources.Add(resource.name); } + } + Debug.Log("[BDArmory.ProjectileUtils]: Couter-measure resources: " + string.Join(", ", _CMResources)); + } + return _CMResources; + } + } + static HashSet _CMResources; + + public static void StealResources(Part hitPart, Vessel sourceVessel, bool thiefWeapon = false) + { + // steal resources if enabled + if (BDArmorySettings.RESOURCE_STEAL_ENABLED || thiefWeapon) + { + if (BDArmorySettings.RESOURCE_STEAL_FUEL_RATION > 0f) StealResource(hitPart.vessel, sourceVessel, FuelResources, BDArmorySettings.RESOURCE_STEAL_FUEL_RATION); + if (BDArmorySettings.RESOURCE_STEAL_AMMO_RATION > 0f) StealResource(hitPart.vessel, sourceVessel, AmmoResources, BDArmorySettings.RESOURCE_STEAL_AMMO_RATION, true); + if (BDArmorySettings.RESOURCE_STEAL_CM_RATION > 0f) StealResource(hitPart.vessel, sourceVessel, CMResources, BDArmorySettings.RESOURCE_STEAL_CM_RATION, true); + } + } + + private class PriorityQueue + { + private Dictionary> partResources = new Dictionary>(); + + public PriorityQueue(HashSet elements) + { + foreach (PartResource r in elements) + { + Add(r); + } + } + + public void Add(PartResource r) + { + int key = r.part.resourcePriorityOffset; + if (partResources.ContainsKey(key)) + { + List existing = partResources[key]; + existing.Add(r); + partResources[key] = existing; + } + else + { + List newList = new List(); + newList.Add(r); + partResources.Add(key, newList); + } + } + + public List Pop() + { + if (partResources.Count == 0) + { + return new List(); + } + int key = partResources.Keys.Max(); + List result = partResources[key]; + partResources.Remove(key); + return result; + } + + public bool HasNext() + { + return partResources.Count != 0; + } + } + + private static void StealResource(Vessel src, Vessel dst, HashSet resourceNames, double ration, bool integerAmounts = false) + { + if (src == null || dst == null) return; + + // identify all parts on source vessel with resource + Dictionary> srcParts = new Dictionary>(); + DeepFind(src.rootPart, resourceNames, srcParts, BDArmorySettings.RESOURCE_STEAL_RESPECT_FLOWSTATE_OUT); + + // identify all parts on destination vessel with resource + Dictionary> dstParts = new Dictionary>(); + DeepFind(dst.rootPart, resourceNames, dstParts, BDArmorySettings.RESOURCE_STEAL_RESPECT_FLOWSTATE_IN); + + foreach (var resourceName in resourceNames) + { + if (!srcParts.ContainsKey(resourceName) || !dstParts.ContainsKey(resourceName)) + { + // if (BDArmorySettings.DEBUG_LABELS) Debug.Log(string.Format("[BDArmory.ProjectileUtils]: Steal resource {0} failed; no parts.", resourceName)); + continue; + } + + double remainingAmount = srcParts[resourceName].Sum(p => p.amount); + if (integerAmounts) + { + remainingAmount = Math.Floor(remainingAmount); + if (remainingAmount == 0) continue; // Nothing left to steal. + } + double amount = remainingAmount * ration; + if (integerAmounts) { amount = Math.Ceiling(amount); } // Round up steal amount so that something is always stolen if there's something to steal. + if (BDArmorySettings.DEBUG_OTHER) Debug.Log("[BDArmory.ProjectileUtils]: " + dst.vesselName + " is trying to steal " + amount.ToString("F1") + " of " + resourceName + " from " + src.vesselName); + + // transfer resource from src->dst parts, honoring their priorities + PriorityQueue sourceQueue = new PriorityQueue(srcParts[resourceName]); + PriorityQueue destinationQueue = new PriorityQueue(dstParts[resourceName]); + List sources = null, destinations = null; + double tolerance = 1e-3; + double amountTaken = 0; + while (amount - amountTaken >= (integerAmounts ? 1d : tolerance)) + { + if (sources == null) + { + sources = sourceQueue.Pop(); + if (sources.Count() == 0) break; + } + if (destinations == null) + { + destinations = destinationQueue.Pop(); + if (destinations.Count() == 0) break; + } + var availability = sources.Where(e => e.amount >= tolerance / sources.Count()); // All source parts with something in. + var opportunity = destinations.Where(e => e.maxAmount - e.amount >= tolerance / destinations.Count()); // All destination parts with room to spare. + if (availability.Count() == 0) { sources = null; } + if (opportunity.Count() == 0) { destinations = null; } + if (sources == null || destinations == null) continue; + if (integerAmounts) + { + if (availability.Sum(e => e.amount) < 1d) { sources = null; } + if (opportunity.Sum(e => e.maxAmount - e.amount) < 1d) { destinations = null; } + if (sources == null || destinations == null) continue; + } + var minFractionAvailable = availability.Min(r => r.amount / r.maxAmount); // Minimum fraction of container size available for transfer. + var minFractionOpportunity = opportunity.Min(r => (r.maxAmount - r.amount) / r.maxAmount); // Minimum fraction of container size available to fill a part. + var totalTransferAvailable = availability.Sum(r => r.maxAmount * minFractionAvailable); + var totalTransferOpportunity = opportunity.Sum(r => r.maxAmount * minFractionOpportunity); + var totalTransfer = Math.Min(amount, Math.Min(totalTransferAvailable, totalTransferOpportunity)); // Total amount to transfer that either transfers the amount required, empties a container or fills a container. + if (integerAmounts) { totalTransfer = Math.Floor(totalTransfer); } + var totalContainerSizeAvailable = availability.Sum(r => r.maxAmount); + var totalContainerSizeOpportunity = opportunity.Sum(r => r.maxAmount); + var transferFractionAvailable = totalTransfer / totalContainerSizeAvailable; + var transferFractionOpportunity = totalTransfer / totalContainerSizeOpportunity; + + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.ProjectileUtils]: Transferring {totalTransfer:F1} of {resourceName} from {string.Join(", ", availability.Select(a => $"{a.part.name} ({a.amount:F1}/{a.maxAmount:F1})").ToList())} on {src.vesselName} to {string.Join(", ", opportunity.Select(o => $"{o.part.name} ({o.amount:F1}/{o.maxAmount:F1})").ToList())} on {dst.vesselName}"); + // Transfer directly between parts doesn't seem to be working properly (it leaves the source, but doesn't arrive at the destination). + var measuredOut = 0d; + var measuredIn = 0d; + foreach (var sourceResource in availability) + { measuredOut += sourceResource.part.TransferResource(sourceResource.info.id, -transferFractionAvailable * sourceResource.maxAmount); } + foreach (var destinationResource in opportunity) + { measuredIn += -destinationResource.part.TransferResource(destinationResource.info.id, transferFractionOpportunity * destinationResource.maxAmount); } + if (Math.Abs(measuredIn - measuredOut) > tolerance) + { Debug.LogWarning($"[BDArmory.ProjectileUtils]: Discrepancy in the amount of {resourceName} transferred from {string.Join(", ", availability.Select(r => r.part.name))} ({measuredOut:F3}) to {string.Join(", ", opportunity.Select(r => r.part.name))} ({measuredIn:F3})"); } + + amountTaken += totalTransfer; + if (totalTransfer < tolerance) + { + Debug.LogWarning($"[BDArmory.ProjectileUtils]: totalTransfer was {totalTransfer} for resource {resourceName}, amount: {amount}, availability: {string.Join(", ", availability.Select(r => r.amount))}, opportunity: {string.Join(", ", opportunity.Select(r => r.maxAmount - r.amount))}"); + if (availability.Sum(r => r.amount) < opportunity.Sum(r => r.maxAmount - r.amount)) { sources = null; } else { destinations = null; } + } + } + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.ProjectileUtils]: Final amount of {resourceName} stolen: {amountTaken:F1}"); + } + } + + private class ResourceAllocation + { + public PartResource sourceResource; + public Part destPart; + public double amount; + public ResourceAllocation(PartResource r, Part p, double a) + { + this.sourceResource = r; + this.destPart = p; + this.amount = a; + } + } + + public static void DeepFind(Part p, HashSet resourceNames, Dictionary> accumulator, bool respectFlowState) + { + foreach (PartResource r in p.Resources) + { + if (resourceNames.Contains(r.resourceName)) + { + if (respectFlowState && !r.flowState) continue; // Ignore locked resources. + if (!accumulator.ContainsKey(r.resourceName)) + accumulator[r.resourceName] = new HashSet(); + accumulator[r.resourceName].Add(r); + } + } + foreach (Part child in p.children) + { + DeepFind(child, resourceNames, accumulator, respectFlowState); + } + } + } +} \ No newline at end of file diff --git a/BDArmory/Utils/SmoothingUtils.cs b/BDArmory/Utils/SmoothingUtils.cs new file mode 100644 index 000000000..15e3a1ad2 --- /dev/null +++ b/BDArmory/Utils/SmoothingUtils.cs @@ -0,0 +1,106 @@ +using UnityEngine; + +namespace BDArmory.Utils +{ + /// + /// Brown's double exponential smoothing (for constant dt between samples, i.e., Holt linear). + /// float version. + /// + public class SmoothingF // .Net 7 will allow using T where: System.IMultiplyOperators, System.IAdditionOperators, System.ISubtractionOperators + { + float S1; + float S2; + float alpha; + float beta; + float rate; + public float Value => 2f * S1 - S2; // The value at the current time. + + /// + /// Constructor for Brown's double exponential smoothing. + /// + /// Smoothing factor. + /// The initial value. + /// The update frequency (for scaling delta in At). + public SmoothingF(float beta, float initialValue = 0, float rate = 0) + { + this.alpha = 1f - beta; + this.beta = beta; + this.rate = rate > 0 ? rate : Time.fixedDeltaTime; + Reset(initialValue); + } + + public void Update(float value) + { + S1 = alpha * value + beta * S1; + S2 = alpha * S1 + beta * S2; + } + + public void Reset(float initialValue = 0) + { + S1 = initialValue; + S2 = initialValue; + } + + /// + /// Estimate the value at a time later than the most recent update. + /// + /// The time difference from now to estimate the value at. + /// The estimated value. + public float At(float delta) + { + var a = 2f * S1 - S2; + var b = alpha / beta * (S1 - S2); + return a + delta / rate * b; + } + } + + /// + /// Brown's double exponential smoothing (for constant dt between samples, i.e., Holt linear). + /// Vector3 version. + /// + public class SmoothingV3 // .Net 7 allows T where: System.IMultiplyOperators, System.IAdditionOperators // Note: we may need to just make multiple versions for float and Vector3 until .Net 7. + { + Vector3 S1; + Vector3 S2; + float alpha; + float beta; + float rate; + public Vector3 Value => 2f * S1 - S2; // The value at the current time. + + /// + /// Constructor for Brown's double exponential smoothing. + /// + /// Smoothing factor. + public SmoothingV3(float beta, Vector3 initialValue = default, float rate = 0) + { + this.alpha = 1f - beta; + this.beta = beta; + this.rate = rate > 0 ? rate : Time.fixedDeltaTime; + Reset(initialValue); + } + + public void Update(Vector3 value) + { + S1 = alpha * value + beta * S1; + S2 = alpha * S1 + beta * S2; + } + + public void Reset(Vector3 initialValue = default) + { + S1 = initialValue; + S2 = initialValue; + } + + /// + /// Estimate the value at a time later than the most recent update. + /// + /// The time difference from now to estimate the value at. + /// The estimated value. + public Vector3 At(float delta) + { + var a = 2f * S1 - S2; + var b = alpha / beta * (S1 - S2); + return a + delta / rate * b; + } + } +} diff --git a/BDArmory/Utils/SoundUtils.cs b/BDArmory/Utils/SoundUtils.cs new file mode 100644 index 000000000..5f76c02da --- /dev/null +++ b/BDArmory/Utils/SoundUtils.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using UnityEngine; + +using BDArmory.Settings; + +namespace BDArmory.Utils +{ + /// + /// Tools to minimise GC and local copies of audioclips. + /// + public static class SoundUtils + { + static Dictionary audioClips = new Dictionary(); // Cache audio clips so that they're not fetched from the GameDatabase every time. Really, the GameDatabase should be doing this! + + /// + /// Get the requested audioclip from the cache. + /// If it's not in the cache, then load the audioclip from the GameDatabase and cache it for future use. + /// + /// The path to a valid audioclip. + /// The AudioClip. + public static AudioClip GetAudioClip(string soundPath) + { + if (!audioClips.TryGetValue(soundPath, out AudioClip audioClip) || audioClip is null) + { + audioClip = GameDatabase.Instance.GetAudioClip(soundPath); + if (audioClip is null) Debug.LogError($"[BDArmory.SoundUtils]: {soundPath} did not give a valid audioclip."); + else if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.SoundUtils]: Adding audioclip {soundPath} to the cache."); + audioClips[soundPath] = audioClip; + } + return audioClip; + } + + /// + /// Reset the cache of audioclips. + /// + public static void ClearAudioCache() => audioClips.Clear(); // Maybe someone has a reason for doing this to reload sounds dynamically? They'd need a way to refresh the GameDatabase too though. + + /// + /// Check whether the soundPath is in the audioclip cache or not and that the audioclip is not null if it is. + /// + /// + /// + public static bool IsCached(string soundPath) => audioClips.ContainsKey(soundPath) && audioClips[soundPath] is not null; + + /// + /// Helper extension to play a one-shot audio clip from the cache based on the sound path. + /// Note: this is equivalent to making a local reference to the AudioClip with SoundUtils.GetAudioClip and playing it via PlayOneShot. + /// + /// The AudioSource. + /// A valid audioclip path. + public static void PlayClipOnce(this AudioSource audioSource, string soundPath) => audioSource.PlayOneShot(GetAudioClip(soundPath)); + } +} \ No newline at end of file diff --git a/BDArmory/Misc/SplineUtils.cs b/BDArmory/Utils/SplineUtils.cs similarity index 99% rename from BDArmory/Misc/SplineUtils.cs rename to BDArmory/Utils/SplineUtils.cs index b3d8e64e8..ee5d46864 100644 --- a/BDArmory/Misc/SplineUtils.cs +++ b/BDArmory/Utils/SplineUtils.cs @@ -1,6 +1,6 @@ using UnityEngine; -namespace BDArmory.Misc +namespace BDArmory.Utils { public static class SplineUtils { diff --git a/BDArmory/Utils/StringUtils.cs b/BDArmory/Utils/StringUtils.cs new file mode 100644 index 000000000..d9151e108 --- /dev/null +++ b/BDArmory/Utils/StringUtils.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using System.Text; +using KSP.Localization; + +namespace BDArmory.Utils +{ + public static class StringUtils + { + static Dictionary localizedStrings = new Dictionary(); // Cache localized strings so that they don't need to be repeatedly localized. + + public static string Localize(string template) + { + if (!localizedStrings.TryGetValue(template, out string result)) + { + result = Localizer.Format(template); + localizedStrings[template] = result; + } + return result; + } + + // static StringBuilder localizedStringBuilder1 = new StringBuilder(); + public static string Localize(string template, params string[] list) => Localizer.Format(template, list); // Don't have a good way to handle <<1>> yet. + // { + // localizedStringBuilder1.Clear(); + // localizedStringBuilder1.Append(Localize(template)); + // for (int i = 0; i < list.Length; ++i) + // { + // localizedStringBuilder1.Append($" {list[i]}"); + // } + // return localizedStringBuilder1.ToString(); + // } + + // static StringBuilder localizedStringBuilder2 = new StringBuilder(); + public static string Localize(string template, params object[] list) => Localizer.Format(template, list); // Don't have a good way to handle <<1>> yet. + // { + // localizedStringBuilder2.Clear(); + // localizedStringBuilder2.Append(Localize(template)); + // for (int i = 0; i < list.Length; ++i) + // { + // localizedStringBuilder2.Append($" {list[i]}"); + // } + // return localizedStringBuilder2.ToString(); + // } + } +} \ No newline at end of file diff --git a/BDArmory/Utils/UIControlUtils.cs b/BDArmory/Utils/UIControlUtils.cs new file mode 100644 index 000000000..7c315b574 --- /dev/null +++ b/BDArmory/Utils/UIControlUtils.cs @@ -0,0 +1,598 @@ +using System.Collections.Generic; +using System.Collections; +using System.Reflection; +using System; +using TMPro; +using UniLinq; +using UnityEngine.SceneManagement; +using UnityEngine.UI; +using UnityEngine; + +namespace BDArmory.Utils +{ + /// + /// Logarithmic FloatRange slider. + /// Gives ranges with values of the form: 0.01, 0.0316, 0.1, 0.316, 1. + /// Specify minValue, maxValue and steps. E.g., (0.01, 1, 4) would give the above sequence. + /// Based on https://github.com/meirumeiru/InfernalRobotics/blob/develop/InfernalRobotics/InfernalRobotics/Gui/UIPartActionFloatEditEx.cs + /// I'm not entirely sure how much of this is necessary. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field)] + public class UI_FloatLogRange : UI_FloatRange + { + private const string UIControlName = "FloatLogRange"; + public int steps = 10; + public UI_FloatLogRange() { } + + /// + /// Update the limits. + /// Call this instead of directly setting minValue/maxValue to properly adjust the slider. + /// + /// + /// + public void UpdateLimits(float minValue, float maxValue) + { + this.minValue = minValue; + this.maxValue = maxValue; + var partActionFieldItem = ((UIPartActionFloatLogRange)this.partActionItem); + if (partActionFieldItem != null) partActionFieldItem.UpdateLimits(); + } + } + + [UI_FloatLogRange] + public class UIPartActionFloatLogRange : UIPartActionFieldItem + { + protected UI_FloatLogRange logFloatRange { get { return (UI_FloatLogRange)control; } } + public TextMeshProUGUI fieldName; + public TextMeshProUGUI fieldValue; + public Slider slider; + private float sliderStepSize; + private bool blockSliderUpdate; + private bool numericSliders = false; + public GameObject numericContainer; + public TextMeshProUGUI fieldNameNumeric; + public TMP_InputField inputField; + private float lastDisplayedValue = 0; + + public static Type VersionTaggedType(Type baseClass) + { + var ass = baseClass.Assembly; + // FIXME The below works to prevent ReflectionTypeLoadException on KSP 1.9, there might be a better way other than OtherUtils.GetLoadableTypes though? + Type tagged = OtherUtils.GetLoadableTypes(ass).Where(t => t.BaseType == baseClass).Where(t => t.FullName.StartsWith(baseClass.FullName)).FirstOrDefault(); + if (tagged != null) + return tagged; + return baseClass; + } + + internal static T GetTaggedComponent(GameObject gameObject) where T : Component + { + return (T)gameObject.GetComponent(VersionTaggedType(typeof(T))); + } + + public static void InstantiateRecursive2(GameObject go, GameObject goc, ref Dictionary list) + { + for (int i = 0; i < go.transform.childCount; i++) + { + list.Add(go.transform.GetChild(i).gameObject, goc.transform.GetChild(i).gameObject); + InstantiateRecursive2(go.transform.GetChild(i).gameObject, goc.transform.GetChild(i).gameObject, ref list); + } + } + + public static void InstantiateRecursive(GameObject go, Transform trfp, ref Dictionary list) + { + for (int i = 0; i < go.transform.childCount; i++) + { + GameObject goc = Instantiate(go.transform.GetChild(i).gameObject); + goc.transform.parent = trfp; + goc.transform.localPosition = go.transform.GetChild(i).localPosition; + if ((goc.transform is RectTransform) && (go.transform.GetChild(i) is RectTransform)) + { + RectTransform rtc = goc.transform as RectTransform; + RectTransform rt = go.transform.GetChild(i) as RectTransform; + + rtc.offsetMax = rt.offsetMax; + rtc.offsetMin = rt.offsetMin; + } + list.Add(go.transform.GetChild(i).gameObject, goc); + InstantiateRecursive2(go.transform.GetChild(i).gameObject, goc, ref list); + } + } + + public static UIPartActionFloatLogRange CreateTemplate() + { + // Create the control + GameObject gameObject = new GameObject("UIPartActionFloatLogRange", VersionTaggedType(typeof(UIPartActionFloatLogRange))); + UIPartActionFloatLogRange partActionFloatLogRange = GetTaggedComponent(gameObject); + gameObject.SetActive(false); + + // Find the template for FloatRange + UIPartActionFloatRange partActionFloatRange = (UIPartActionFloatRange)UIPartActionController.Instance.fieldPrefabs.Find(cls => cls.GetType() == typeof(UIPartActionFloatRange)); + + // Copy UI elements + RectTransform rtc = gameObject.AddComponent(); + RectTransform rt = partActionFloatRange.transform as RectTransform; + rtc.offsetMin = rt.offsetMin; + rtc.offsetMax = rt.offsetMax; + rtc.anchorMin = rt.anchorMin; + rtc.anchorMax = rt.anchorMax; + LayoutElement lec = gameObject.AddComponent(); + LayoutElement le = partActionFloatRange.GetComponent(); + lec.flexibleHeight = le.flexibleHeight; + lec.flexibleWidth = le.flexibleWidth; + lec.minHeight = le.minHeight; + lec.minWidth = le.minWidth; + lec.preferredHeight = le.preferredHeight; + lec.preferredWidth = le.preferredWidth; + lec.layoutPriority = le.layoutPriority; + + // Copy control elements + Dictionary list = new Dictionary(); + InstantiateRecursive(partActionFloatRange.gameObject, gameObject.transform, ref list); + list.TryGetValue(partActionFloatRange.fieldName.gameObject, out GameObject fieldNameGO); + partActionFloatLogRange.fieldName = fieldNameGO.GetComponent(); + list.TryGetValue(partActionFloatRange.fieldAmount.gameObject, out GameObject fieldValueGO); + partActionFloatLogRange.fieldValue = fieldValueGO.GetComponent(); + list.TryGetValue(partActionFloatRange.slider.gameObject, out GameObject sliderGO); + partActionFloatLogRange.slider = sliderGO.GetComponent(); + list.TryGetValue(partActionFloatRange.numericContainer, out partActionFloatLogRange.numericContainer); + list.TryGetValue(partActionFloatRange.inputField.gameObject, out GameObject inputFieldGO); + partActionFloatLogRange.inputField = inputFieldGO.GetComponent(); + list.TryGetValue(partActionFloatRange.fieldNameNumeric.gameObject, out GameObject fieldNameNumericGO); + partActionFloatLogRange.fieldNameNumeric = fieldNameNumericGO.GetComponent(); + + return partActionFloatLogRange; + } + + public override void Setup(UIPartActionWindow window, Part part, PartModule partModule, UI_Scene scene, UI_Control control, BaseField field) + { + base.Setup(window, part, partModule, scene, control, field); + UpdateLimits(); + fieldName.text = field.guiName; + fieldNameNumeric.text = field.guiName; + float value = GetFieldValue(); + SetFieldValue(value); + UpdateDisplay(value); + // Debug.Log($"DEBUG value is {value} with limits {logFloatRange.minValue}—{logFloatRange.maxValue}"); + // Debug.Log($"DEBUG slider has value {slider.value} with limits {slider.minValue}—{slider.maxValue}"); + slider.onValueChanged.AddListener(OnValueChanged); + inputField.onValueChanged.AddListener(OnNumericValueChanged); + inputField.onSubmit.AddListener(OnNumericSubmitted); + inputField.onSelect.AddListener(OnNumericSelected); + inputField.onDeselect.AddListener(OnNumericDeselected); + } + + private float GetFieldValue() + { + float value = field.GetValue(field.host); + return value; + } + private float UpdateSlider(float value) + { + // Note: We use Log10 here as it has better human-centric rounding properties (i.e., 0.001 instead of 0.000999999999). + value = Mathf.Pow(10f, Mathf.Clamp(BDAMath.RoundToUnit(Mathf.Log10(value) - slider.minValue, sliderStepSize) + slider.minValue, slider.minValue, slider.maxValue)); + // Debug.Log($"DEBUG Slider updated to {value}"); + return value; + } + private void UpdateDisplay(float value) + { + if (numericSliders != Window.NumericSliders) + { + numericSliders = Window.NumericSliders; + slider.gameObject.SetActive(!Window.NumericSliders); + numericContainer.SetActive(Window.NumericSliders); + } + blockSliderUpdate = true; + lastDisplayedValue = value; + fieldValue.text = value.ToString("G3"); + if (numericSliders) + { inputField.text = fieldValue.text; } + else + { slider.value = Mathf.Log10(value); } + blockSliderUpdate = false; + } + private void OnValueChanged(float obj) + { + if (blockSliderUpdate) return; + if (control is not null && control.requireFullControl) + { if (!InputLockManager.IsUnlocked(ControlTypes.TWEAKABLES_FULLONLY)) return; } + else + { if (!InputLockManager.IsUnlocked(ControlTypes.TWEAKABLES_ANYCONTROL)) return; } + float value = Mathf.Pow(10f, slider.value); + value = UpdateSlider(value); + SetFieldValue(value); + UpdateDisplay(value); + } + private void OnNumericSubmitted(string str) + { + if (float.TryParse(str, out float value)) + { + value = Mathf.Clamp(value, logFloatRange.minValue, logFloatRange.maxValue); // Clamp, but don't round the value when in numeric mode. + SetFieldValue(value); + UpdateDisplay(value); + } + } + void OnNumericValueChanged(string str) + { + if (inputField.wasCanceled) OnNumericSubmitted(str); + } + void OnNumericSelected(string str) + { + AddInputFieldLock(str); + } + void OnNumericDeselected(string str) + { + OnNumericSubmitted(str); + RemoveInputfieldLock(); + } + + public override void UpdateItem() + { + float value = GetFieldValue(); + if (value == lastDisplayedValue && numericSliders == Window.NumericSliders) return; // Do nothing if the value hasn't changed or the # hasn't been toggled. + // fieldName.text = field.guiName; // Label doesn't update. + UpdateDisplay(value); + } + + /// + /// Update the limits of the slider. + /// Call this whenever the min/max values of the underlying field are changed. + /// + public void UpdateLimits() + { + var value = GetFieldValue(); // Store the current value so it doesn't get clamped. + blockSliderUpdate = true; // Block the slider from updating while we reset the value. + slider.minValue = Mathf.Log10(logFloatRange.minValue); + slider.maxValue = Mathf.Log10(logFloatRange.maxValue); + sliderStepSize = (slider.maxValue - slider.minValue) / logFloatRange.steps; + logFloatRange.stepIncrement = sliderStepSize; + SetFieldValue(value); // Restore the unclamped value. + UpdateDisplay(value); + } + } + + [KSPAddon(KSPAddon.Startup.Instantly, true)] + internal class UIPartActionFloatLogRangeRegistration : MonoBehaviour + { + private static bool loaded = false; + private static bool isRunning = false; + private Coroutine register = null; + public void Start() + { + if (loaded) + { + Destroy(gameObject); + return; + } + loaded = true; + DontDestroyOnLoad(gameObject); + SceneManager.sceneLoaded += OnLevelFinishedLoading; + } + + public void OnLevelFinishedLoading(Scene scene, LoadSceneMode mode) + { + if (isRunning) StopCoroutine("Register"); + if (!(HighLogic.LoadedSceneIsEditor || HighLogic.LoadedSceneIsFlight)) return; + isRunning = true; + register = StartCoroutine(Register()); + } + + internal IEnumerator Register() + { + UIPartActionController controller; + while ((controller = UIPartActionController.Instance) is null) yield return null; + + FieldInfo typesField = (from fld in controller.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance) + where fld.FieldType == typeof(List) + select fld).First(); + + List fieldPrefabTypes; + while ((fieldPrefabTypes = (List)typesField.GetValue(controller)) == null + || fieldPrefabTypes.Count == 0 + || !UIPartActionController.Instance.fieldPrefabs.Find(cls => cls.GetType() == typeof(UIPartActionFloatRange))) + yield return false; + + // Register prefabs + controller.fieldPrefabs.Add(UIPartActionFloatLogRange.CreateTemplate()); + fieldPrefabTypes.Add(typeof(UI_FloatLogRange)); + + isRunning = false; + } + } + + /// + /// Semi-Logarithmic FloatRange slider. + /// Gives ranges where the values are of the form: 0.9, 1, 2, ..., 9, 10, 20, ..., 90, 100, 200, ..., 900, 1000, 2000. + /// Specify minValue, maxValue and sigFig. The stepIncrement is automatically calculated. + /// Based on the Logarithmic FloatRange slider above. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field)] + public class UI_FloatSemiLogRange : UI_FloatRange + { + private const string UIControlName = "FloatSemiLogRange"; + public int sigFig = 2; // 2 sig.fig. gives: ..., 9.8, 9.9, 10, 11, 12, ... + public UI_FloatSemiLogRange() { } + + /// + /// Update the limits. + /// Call this instead of directly setting min/max value or sigFig to properly update the slider. + /// + /// + /// + /// + public void UpdateLimits(float minValue, float maxValue, int sigFig = 0) + { + this.minValue = minValue; + this.maxValue = maxValue; + if (sigFig > 0) this.sigFig = sigFig; + var partActionFieldItem = ((UIPartActionFloatSemiLogRange)this.partActionItem); + if (partActionFieldItem != null) partActionFieldItem.UpdateLimits(); + } + } + + [UI_FloatSemiLogRange] + public class UIPartActionFloatSemiLogRange : UIPartActionFieldItem + { + protected UI_FloatSemiLogRange semiLogFloatRange { get { return (UI_FloatSemiLogRange)control; } } + public TextMeshProUGUI fieldName; + public TextMeshProUGUI fieldValue; + public Slider slider; + private float sliderStepSize; + private float minStepSize; + private float maxStepSize; + private bool blockSliderUpdate; + private bool numericSliders = false; + private string fieldFormatString = "G3"; + public GameObject numericContainer; + public TextMeshProUGUI fieldNameNumeric; + public TMP_InputField inputField; + private float lastDisplayedValue = 0; + + public static Type VersionTaggedType(Type baseClass) + { + var ass = baseClass.Assembly; + // FIXME The below works to prevent ReflectionTypeLoadException on KSP 1.9, there might be a better way other than OtherUtils.GetLoadableTypes though? + Type tagged = OtherUtils.GetLoadableTypes(ass).Where(t => t.BaseType == baseClass).Where(t => t.FullName.StartsWith(baseClass.FullName)).FirstOrDefault(); + if (tagged != null) + return tagged; + return baseClass; + } + + internal static T GetTaggedComponent(GameObject gameObject) where T : Component + { + return (T)gameObject.GetComponent(VersionTaggedType(typeof(T))); + } + + public static void InstantiateRecursive2(GameObject go, GameObject goc, ref Dictionary list) + { + for (int i = 0; i < go.transform.childCount; i++) + { + list.Add(go.transform.GetChild(i).gameObject, goc.transform.GetChild(i).gameObject); + InstantiateRecursive2(go.transform.GetChild(i).gameObject, goc.transform.GetChild(i).gameObject, ref list); + } + } + + public static void InstantiateRecursive(GameObject go, Transform trfp, ref Dictionary list) + { + for (int i = 0; i < go.transform.childCount; i++) + { + GameObject goc = Instantiate(go.transform.GetChild(i).gameObject); + goc.transform.parent = trfp; + goc.transform.localPosition = go.transform.GetChild(i).localPosition; + if ((goc.transform is RectTransform) && (go.transform.GetChild(i) is RectTransform)) + { + RectTransform rtc = goc.transform as RectTransform; + RectTransform rt = go.transform.GetChild(i) as RectTransform; + + rtc.offsetMax = rt.offsetMax; + rtc.offsetMin = rt.offsetMin; + } + list.Add(go.transform.GetChild(i).gameObject, goc); + InstantiateRecursive2(go.transform.GetChild(i).gameObject, goc, ref list); + } + } + + public static UIPartActionFloatSemiLogRange CreateTemplate() + { + // Create the control + GameObject gameObject = new GameObject("UIPartActionFloatSemiLogRange", VersionTaggedType(typeof(UIPartActionFloatSemiLogRange))); + UIPartActionFloatSemiLogRange partActionFloatSemiLogRange = GetTaggedComponent(gameObject); + gameObject.SetActive(false); + + // Find the template for FloatRange + UIPartActionFloatRange partActionFloatRange = (UIPartActionFloatRange)UIPartActionController.Instance.fieldPrefabs.Find(cls => cls.GetType() == typeof(UIPartActionFloatRange)); + + // Copy UI elements + RectTransform rtc = gameObject.AddComponent(); + RectTransform rt = partActionFloatRange.transform as RectTransform; + rtc.offsetMin = rt.offsetMin; + rtc.offsetMax = rt.offsetMax; + rtc.anchorMin = rt.anchorMin; + rtc.anchorMax = rt.anchorMax; + LayoutElement lec = gameObject.AddComponent(); + LayoutElement le = partActionFloatRange.GetComponent(); + lec.flexibleHeight = le.flexibleHeight; + lec.flexibleWidth = le.flexibleWidth; + lec.minHeight = le.minHeight; + lec.minWidth = le.minWidth; + lec.preferredHeight = le.preferredHeight; + lec.preferredWidth = le.preferredWidth; + lec.layoutPriority = le.layoutPriority; + + // Copy control elements + Dictionary list = new Dictionary(); + InstantiateRecursive(partActionFloatRange.gameObject, gameObject.transform, ref list); + list.TryGetValue(partActionFloatRange.fieldName.gameObject, out GameObject fieldNameGO); + partActionFloatSemiLogRange.fieldName = fieldNameGO.GetComponent(); + list.TryGetValue(partActionFloatRange.fieldAmount.gameObject, out GameObject fieldValueGO); + partActionFloatSemiLogRange.fieldValue = fieldValueGO.GetComponent(); + list.TryGetValue(partActionFloatRange.slider.gameObject, out GameObject sliderGO); + partActionFloatSemiLogRange.slider = sliderGO.GetComponent(); + list.TryGetValue(partActionFloatRange.numericContainer, out partActionFloatSemiLogRange.numericContainer); + list.TryGetValue(partActionFloatRange.inputField.gameObject, out GameObject inputFieldGO); + partActionFloatSemiLogRange.inputField = inputFieldGO.GetComponent(); + list.TryGetValue(partActionFloatRange.fieldNameNumeric.gameObject, out GameObject fieldNameNumericGO); + partActionFloatSemiLogRange.fieldNameNumeric = fieldNameNumericGO.GetComponent(); + + return partActionFloatSemiLogRange; + } + + public override void Setup(UIPartActionWindow window, Part part, PartModule partModule, UI_Scene scene, UI_Control control, BaseField field) + { + base.Setup(window, part, partModule, scene, control, field); + UpdateLimits(); + fieldName.text = field.guiName; + fieldNameNumeric.text = field.guiName; + fieldFormatString = $"G{semiLogFloatRange.sigFig + 2}"; // Show at most 2 digits beyond the requested sig. fig. + float value = GetFieldValue(); + SetFieldValue(value); + UpdateDisplay(value); + // Debug.Log($"DEBUG value is {value} with limits {semiLogFloatRange.minValue}—{semiLogFloatRange.maxValue}"); + // Debug.Log($"DEBUG slider has value {slider.value} with limits {slider.minValue}—{slider.maxValue}"); + slider.onValueChanged.AddListener(OnValueChanged); + inputField.onValueChanged.AddListener(OnNumericValueChanged); + inputField.onSubmit.AddListener(OnNumericSubmitted); + inputField.onSelect.AddListener(OnNumericSelected); + inputField.onDeselect.AddListener(OnNumericDeselected); + } + + private float GetFieldValue() + { + float value = field.GetValue(field.host); + return value; + } + private float UpdateSlider(float value) + { + var fromValue = FromSemiLogValue(value); + var roundedValue = BDAMath.RoundToUnit(fromValue, sliderStepSize); + var toValue = ToSemiLogValue(roundedValue); + // Debug.Log($"DEBUG SemiLog: value {value} -> {fromValue} -> {roundedValue} -> {toValue}"); + return toValue; + } + float ToSemiLogValue(float value) => Mathf.Pow(10f, Mathf.Floor((value - 1f) / 9f)) * (1f + (value - 1f) % 9f) * minStepSize; + float FromSemiLogValue(float value) { value /= minStepSize; return Mathf.Floor(Mathf.Log10(value)) * 9 + value / Mathf.Pow(10, Mathf.Floor(Mathf.Log10(value))); } + private void UpdateDisplay(float value) + { + if (numericSliders != Window.NumericSliders) + { + numericSliders = Window.NumericSliders; + slider.gameObject.SetActive(!numericSliders); + numericContainer.SetActive(numericSliders); + } + blockSliderUpdate = true; + lastDisplayedValue = value; + fieldValue.text = value.ToString(fieldFormatString); + if (numericSliders) + { inputField.text = fieldValue.text; } + else + { slider.value = FromSemiLogValue(value); } + blockSliderUpdate = false; + } + private void OnValueChanged(float obj) + { + if (blockSliderUpdate) return; + if (control is not null && control.requireFullControl) + { if (!InputLockManager.IsUnlocked(ControlTypes.TWEAKABLES_FULLONLY)) return; } + else + { if (!InputLockManager.IsUnlocked(ControlTypes.TWEAKABLES_ANYCONTROL)) return; } + float value = ToSemiLogValue(slider.value); + value = UpdateSlider(value); + SetFieldValue(value); + UpdateDisplay(value); + } + private void OnNumericSubmitted(string str) + { + if (float.TryParse(str, out float value)) + { + value = Mathf.Clamp(value, semiLogFloatRange.minValue, semiLogFloatRange.maxValue); // Clamp, but don't round the value when in numeric mode. + SetFieldValue(value); + UpdateDisplay(value); + } + } + void OnNumericValueChanged(string str) + { + if (inputField.wasCanceled) OnNumericSubmitted(str); + } + void OnNumericSelected(string str) + { + AddInputFieldLock(str); + } + void OnNumericDeselected(string str) + { + OnNumericSubmitted(str); + RemoveInputfieldLock(); + } + + public override void UpdateItem() + { + float value = GetFieldValue(); + if (value == lastDisplayedValue && numericSliders == Window.NumericSliders) return; // Do nothing if the value hasn't changed or the # hasn't been toggled. + // fieldName.text = field.guiName; // Label doesn't update. + UpdateDisplay(value); + } + + public void UpdateLimits() + { + var value = GetFieldValue(); // Store the current value so it doesn't get clamped. + var minStepSizePower = Mathf.Floor(Mathf.Log10(semiLogFloatRange.minValue)); + var maxStepSizePower = Mathf.Floor(Mathf.Log10(semiLogFloatRange.maxValue)); + minStepSize = Mathf.Pow(10, minStepSizePower); + maxStepSize = Mathf.Pow(10, maxStepSizePower); + blockSliderUpdate = true; // Block the slider from updating while we adjust things (unblocks in UpdateDisplay). + sliderStepSize = Mathf.Pow(10, 1 - semiLogFloatRange.sigFig); + slider.minValue = BDAMath.RoundToUnit(semiLogFloatRange.minValue / minStepSize, sliderStepSize); + slider.maxValue = BDAMath.RoundToUnit(9f * (maxStepSizePower - minStepSizePower) + semiLogFloatRange.maxValue / maxStepSize, sliderStepSize); + semiLogFloatRange.stepIncrement = sliderStepSize; + SetFieldValue(value); // Restore the unclamped value. + UpdateDisplay(value); + } + } + + [KSPAddon(KSPAddon.Startup.Instantly, true)] + internal class UIPartActionFloatSemiLogRangeRegistration : MonoBehaviour + { + private static bool loaded = false; + private static bool isRunning = false; + private Coroutine register = null; + public void Start() + { + if (loaded) + { + Destroy(gameObject); + return; + } + loaded = true; + DontDestroyOnLoad(gameObject); + SceneManager.sceneLoaded += OnLevelFinishedLoading; + } + + public void OnLevelFinishedLoading(Scene scene, LoadSceneMode mode) + { + if (isRunning) StopCoroutine("Register"); + if (!(HighLogic.LoadedSceneIsEditor || HighLogic.LoadedSceneIsFlight)) return; + isRunning = true; + register = StartCoroutine(Register()); + } + + internal IEnumerator Register() + { + UIPartActionController controller; + while ((controller = UIPartActionController.Instance) is null) yield return null; + + FieldInfo typesField = (from fld in controller.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance) + where fld.FieldType == typeof(List) + select fld).First(); + + List fieldPrefabTypes; + while ((fieldPrefabTypes = (List)typesField.GetValue(controller)) == null + || fieldPrefabTypes.Count == 0 + || !UIPartActionController.Instance.fieldPrefabs.Find(cls => cls.GetType() == typeof(UIPartActionFloatRange))) + yield return false; + + // Register prefabs + controller.fieldPrefabs.Add(UIPartActionFloatSemiLogRange.CreateTemplate()); + fieldPrefabTypes.Add(typeof(UI_FloatSemiLogRange)); + + isRunning = false; + } + } +} diff --git a/BDArmory/Misc/VectorUtils.cs b/BDArmory/Utils/VectorUtils.cs similarity index 82% rename from BDArmory/Misc/VectorUtils.cs rename to BDArmory/Utils/VectorUtils.cs index fcc3e8946..b4c62fa77 100644 --- a/BDArmory/Misc/VectorUtils.cs +++ b/BDArmory/Utils/VectorUtils.cs @@ -1,7 +1,9 @@ using System; using UnityEngine; -namespace BDArmory.Misc +using BDArmory.Extensions; + +namespace BDArmory.Utils { public static class VectorUtils { @@ -52,7 +54,7 @@ public static float CalculateLeadTime(Vector3 delta, Vector3 vr, float muzzleV) // If the determinant is negative, then there is no solution if (det > 0f) { - return 2f * c / (Mathf.Sqrt(det) - b); + return 2f * c / (BDAMath.Sqrt(det) - b); } else { @@ -86,7 +88,7 @@ public static Vector3 WeightedDirectionDeviation(Vector3 direction, float maxAng float random = UnityEngine.Random.Range(0f, 1f); float maxRotate = maxAngle * (random * random); maxRotate = Mathf.Clamp(maxRotate, 0, maxAngle) * Mathf.Deg2Rad; - return Vector3.RotateTowards(direction, Vector3.ProjectOnPlane(UnityEngine.Random.onUnitSphere, direction), maxRotate, 0).normalized; + return Vector3.RotateTowards(direction, UnityEngine.Random.onUnitSphere.ProjectOnPlane(direction), maxRotate, 0).normalized; } /// @@ -117,7 +119,7 @@ public static float Gaussian() // Technically this will raise an exception if the first random produces a zero (which should never happen now that it's log(1-rnd)) try { - return Mathf.Sqrt(-2 * Mathf.Log(1f - UnityEngine.Random.value)) * Mathf.Cos(Mathf.PI * UnityEngine.Random.value); + return BDAMath.Sqrt(-2 * Mathf.Log(1f - UnityEngine.Random.value)) * Mathf.Cos(Mathf.PI * UnityEngine.Random.value); } catch (Exception e) { // I have no idea what exception Mathf.Log raises when it gets a zero @@ -126,6 +128,15 @@ public static float Gaussian() } } + /// + /// Generate a Vector3 with elements from an approximately normal distribution (mean: 0, std.dev: 1). + /// + /// + public static Vector3 GaussianVector3() + { + return new Vector3(Gaussian(), Gaussian(), Gaussian()); + } + public static Vector3d GaussianVector3d(Vector3d mean, Vector3d stdDev) { return new Vector3d( @@ -147,7 +158,7 @@ public static float Rayleigh() // Technically this will raise an exception if the random produces a zero, which should almost never happen try { - return Mathf.Sqrt(-2 * Mathf.Log(UnityEngine.Random.value)); + return BDAMath.Sqrt(-2 * Mathf.Log(UnityEngine.Random.value)); } catch (Exception e) { // I have no idea what exception Mathf.Log raises when it gets a zero @@ -228,8 +239,8 @@ public static float GeoDistance(Vector3 start, Vector3 destination, CelestialBod float dlat = lat2 - lat1; float dlon = (destination.y - start.y) * Mathf.Deg2Rad; float a = Mathf.Sin(dlat / 2) * Mathf.Sin(dlat / 2) + Mathf.Cos(lat1) * Mathf.Cos(lat2) * Mathf.Sin(dlon / 2) * Mathf.Sin(dlon / 2); - float distance = 2 * Mathf.Atan2(Mathf.Sqrt(a), Mathf.Sqrt(1 - a)) * (float)body.Radius; - return Mathf.Sqrt(distance * distance + (destination.z - start.z) * (destination.z - start.z)); + float distance = 2 * Mathf.Atan2(BDAMath.Sqrt(a), BDAMath.Sqrt(1 - a)) * (float)body.Radius; + return BDAMath.Sqrt(distance * distance + (destination.z - start.z) * (destination.z - start.z)); } public static Vector3 RotatePointAround(Vector3 pointToRotate, Vector3 pivotPoint, Vector3 axis, float angle) @@ -241,10 +252,28 @@ public static Vector3 RotatePointAround(Vector3 pointToRotate, Vector3 pivotPoin public static Vector3 GetNorthVector(Vector3 position, CelestialBody body) { - Vector3 geoPosA = WorldPositionToGeoCoords(position, body); - Vector3 geoPosB = new Vector3(geoPosA.x + 1, geoPosA.y, geoPosA.z); - Vector3 north = GetWorldSurfacePostion(geoPosB, body) - GetWorldSurfacePostion(geoPosA, body); - return Vector3.ProjectOnPlane(north, body.GetSurfaceNVector(geoPosA.x, geoPosA.y)).normalized; + var latlon = body.GetLatitudeAndLongitude(position); + var surfacePoint = body.GetWorldSurfacePosition(latlon.x, latlon.y, 0); + var up = (body.GetWorldSurfacePosition(latlon.x, latlon.y, 1000) - surfacePoint).normalized; + var north = -Math.Sign(latlon.x) * (body.GetWorldSurfacePosition(latlon.x - Math.Sign(latlon.x), latlon.y, 0) - surfacePoint).ProjectOnPlanePreNormalized(up).normalized; + return north; + } + + /// + /// Efficiently calculate up, north and right at a given worldspace position on a body. + /// + /// + /// + /// + /// + /// + public static void GetWorldCoordinateFrame(CelestialBody body, Vector3 position, out Vector3 up, out Vector3 north, out Vector3 right) + { + var latlon = body.GetLatitudeAndLongitude(position); + var surfacePoint = body.GetWorldSurfacePosition(latlon.x, latlon.y, 0); + up = (body.GetWorldSurfacePosition(latlon.x, latlon.y, 1000) - surfacePoint).normalized; + north = -Math.Sign(latlon.x) * (body.GetWorldSurfacePosition(latlon.x - Math.Sign(latlon.x), latlon.y, 0) - surfacePoint).ProjectOnPlanePreNormalized(up).normalized; + right = Vector3.Cross(up, north); } public static Vector3 GetWorldSurfacePostion(Vector3d geoPosition, CelestialBody body) @@ -271,7 +300,8 @@ public static bool SphereRayIntersect(Ray ray, Vector3 sphereCenter, double sphe double d; - d = -(Vector3.Dot(l, o - c) + Math.Sqrt(Mathf.Pow(Vector3.Dot(l, o - c), 2) - (o - c).sqrMagnitude + (r * r))); + var dotLOC = Vector3.Dot(l, o - c); + d = -(Vector3.Dot(l, o - c) + Math.Sqrt(dotLOC * dotLOC - (o - c).sqrMagnitude + (r * r))); if (double.IsNaN(d)) { diff --git a/BDArmory/Modules/VesselModuleRegistry.cs b/BDArmory/Utils/VesselModuleRegistry.cs similarity index 81% rename from BDArmory/Modules/VesselModuleRegistry.cs rename to BDArmory/Utils/VesselModuleRegistry.cs index 16c57b3ad..13a48536d 100644 --- a/BDArmory/Modules/VesselModuleRegistry.cs +++ b/BDArmory/Utils/VesselModuleRegistry.cs @@ -3,12 +3,14 @@ using System.Collections.Generic; using System.Linq; using UnityEngine; -using BDArmory.Core; + using BDArmory.Competition; using BDArmory.Control; -using BDArmory.Misc; +using BDArmory.Settings; +using BDArmory.Weapons; +using BDArmory.Weapons.Missiles; -namespace BDArmory.Modules +namespace BDArmory.Utils { /// /// A registry over all the asked for modules in all the asked for vessels. @@ -41,6 +43,7 @@ public class VesselModuleRegistry : MonoBehaviour static public Dictionary> registryModuleCommand; static public Dictionary> registryKerbalSeat; static public Dictionary> registryKerbalEVA; + static public Dictionary> registryRepulsorModule; static Dictionary vesselPartCounts; #endregion @@ -64,7 +67,7 @@ void Awake() if (registryModuleCommand == null) { registryModuleCommand = new Dictionary>(); } if (registryKerbalSeat == null) { registryKerbalSeat = new Dictionary>(); } if (registryKerbalEVA == null) { registryKerbalEVA = new Dictionary>(); } - + if (registryRepulsorModule == null) { registryRepulsorModule = new Dictionary>(); } if (updateModuleCallbacks == null) { updateModuleCallbacks = new Dictionary(); } if (vesselPartCounts == null) { vesselPartCounts = new Dictionary(); } } @@ -91,6 +94,7 @@ void OnDestroy() registryModuleCommand.Clear(); registryKerbalSeat.Clear(); registryKerbalEVA.Clear(); + registryRepulsorModule.Clear(); updateModuleCallbacks.Clear(); vesselPartCounts.Clear(); @@ -106,7 +110,7 @@ void AddVesselToRegistry(Vessel vessel) { registry.Add(vessel, new Dictionary>()); vesselPartCounts[vessel] = vessel.Parts.Count; - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log($"[BDArmory.VesselModuleRegistry]: Vessel {vessel.vesselName} added to registry."); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.VesselModuleRegistry]: Vessel {vessel.vesselName} added to registry."); } /// @@ -133,7 +137,7 @@ void UpdateVesselModulesInRegistry(Vessel vessel) where T : class if (!registry.ContainsKey(vessel)) { AddVesselToRegistry(vessel); } if (!registry[vessel].ContainsKey(typeof(T))) { AddVesselModuleTypeToRegistry(vessel); } registry[vessel][typeof(T)] = vessel.FindPartModulesImplementing().ConvertAll(m => m as UnityEngine.Object); - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log($"[BDArmory.VesselModuleRegistry]: Registry entry for {vessel.vesselName} updated to have {registry[vessel][typeof(T)].Count} modules of type {typeof(T).Name}."); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.VesselModuleRegistry]: Registry entry for {vessel.vesselName} updated to have {registry[vessel][typeof(T)].Count} modules of type {typeof(T).Name}."); } /// @@ -162,62 +166,67 @@ public void OnVesselModifiedHandler(Vessel vessel) if (registryMissileFire.ContainsKey(vessel) && (partsAdded || registryMissileFire[vessel].Count > 0)) { registryMissileFire[vessel] = vessel.FindPartModulesImplementing(); - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log($"[BDArmory.VesselModuleRegistry]: Specialised registry entry for {vessel.vesselName} updated to have {registryMissileFire[vessel].Count} modules of type {typeof(MissileFire).Name}."); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.VesselModuleRegistry]: Specialised registry entry for {vessel.vesselName} updated to have {registryMissileFire[vessel].Count} modules of type {typeof(MissileFire).Name}."); } if (registryMissileBase.ContainsKey(vessel) && (partsAdded || registryMissileBase[vessel].Count > 0)) { registryMissileBase[vessel] = vessel.FindPartModulesImplementing(); - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log($"[BDArmory.VesselModuleRegistry]: Specialised registry entry for {vessel.vesselName} updated to have {registryMissileBase[vessel].Count} modules of type {typeof(MissileBase).Name}."); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.VesselModuleRegistry]: Specialised registry entry for {vessel.vesselName} updated to have {registryMissileBase[vessel].Count} modules of type {typeof(MissileBase).Name}."); } if (registryBDModulePilotAI.ContainsKey(vessel) && (partsAdded || registryBDModulePilotAI[vessel].Count > 0)) { registryBDModulePilotAI[vessel] = vessel.FindPartModulesImplementing(); - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log($"[BDArmory.VesselModuleRegistry]: Specialised registry entry for {vessel.vesselName} updated to have {registryBDModulePilotAI[vessel].Count} modules of type {typeof(BDModulePilotAI).Name}."); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.VesselModuleRegistry]: Specialised registry entry for {vessel.vesselName} updated to have {registryBDModulePilotAI[vessel].Count} modules of type {typeof(BDModulePilotAI).Name}."); } if (registryBDModuleSurfaceAI.ContainsKey(vessel) && (partsAdded || registryBDModuleSurfaceAI[vessel].Count > 0)) { registryBDModuleSurfaceAI[vessel] = vessel.FindPartModulesImplementing(); - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log($"[BDArmory.VesselModuleRegistry]: Specialised registry entry for {vessel.vesselName} updated to have {registryBDModuleSurfaceAI[vessel].Count} modules of type {typeof(BDModuleSurfaceAI).Name}."); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.VesselModuleRegistry]: Specialised registry entry for {vessel.vesselName} updated to have {registryBDModuleSurfaceAI[vessel].Count} modules of type {typeof(BDModuleSurfaceAI).Name}."); } if (registryIBDAIControl.ContainsKey(vessel) && (partsAdded || registryIBDAIControl[vessel].Count > 0)) { registryIBDAIControl[vessel] = vessel.FindPartModulesImplementing(); - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log($"[BDArmory.VesselModuleRegistry]: Specialised registry entry for {vessel.vesselName} updated to have {registryIBDAIControl[vessel].Count} modules of type {typeof(IBDAIControl).Name}."); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.VesselModuleRegistry]: Specialised registry entry for {vessel.vesselName} updated to have {registryIBDAIControl[vessel].Count} modules of type {typeof(IBDAIControl).Name}."); } if (registryModuleWeapon.ContainsKey(vessel) && (partsAdded || registryModuleWeapon[vessel].Count > 0)) { registryModuleWeapon[vessel] = vessel.FindPartModulesImplementing(); - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log($"[BDArmory.VesselModuleRegistry]: Specialised registry entry for {vessel.vesselName} updated to have {registryModuleWeapon[vessel].Count} modules of type {typeof(ModuleWeapon).Name}."); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.VesselModuleRegistry]: Specialised registry entry for {vessel.vesselName} updated to have {registryModuleWeapon[vessel].Count} modules of type {typeof(ModuleWeapon).Name}."); } if (registryIBDWeapon.ContainsKey(vessel) && (partsAdded || registryIBDWeapon[vessel].Count > 0)) { registryIBDWeapon[vessel] = vessel.FindPartModulesImplementing(); - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log($"[BDArmory.VesselModuleRegistry]: Specialised registry entry for {vessel.vesselName} updated to have {registryIBDWeapon[vessel].Count} modules of type {typeof(IBDWeapon).Name}."); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.VesselModuleRegistry]: Specialised registry entry for {vessel.vesselName} updated to have {registryIBDWeapon[vessel].Count} modules of type {typeof(IBDWeapon).Name}."); } if (registryModuleEngines.ContainsKey(vessel) && (partsAdded || registryModuleEngines[vessel].Count > 0)) { registryModuleEngines[vessel] = vessel.FindPartModulesImplementing(); - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log($"[BDArmory.VesselModuleRegistry]: Specialised registry entry for {vessel.vesselName} updated to have {registryModuleEngines[vessel].Count} modules of type {typeof(ModuleEngines).Name}."); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.VesselModuleRegistry]: Specialised registry entry for {vessel.vesselName} updated to have {registryModuleEngines[vessel].Count} modules of type {typeof(ModuleEngines).Name}."); } if (registryModuleIntakes.ContainsKey(vessel) && (partsAdded || registryModuleIntakes[vessel].Count > 0)) { registryModuleIntakes[vessel] = vessel.FindPartModulesImplementing(); - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log($"[BDArmory.VesselModuleRegistry]: Specialised registry entry for {vessel.vesselName} updated to have {registryModuleIntakes[vessel].Count} modules of type {typeof(ModuleResourceIntake).Name}."); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.VesselModuleRegistry]: Specialised registry entry for {vessel.vesselName} updated to have {registryModuleIntakes[vessel].Count} modules of type {typeof(ModuleResourceIntake).Name}."); } if (registryModuleCommand.ContainsKey(vessel) && (partsAdded || registryModuleCommand[vessel].Count > 0)) { registryModuleCommand[vessel] = vessel.FindPartModulesImplementing(); - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log($"[BDArmory.VesselModuleRegistry]: Specialised registry entry for {vessel.vesselName} updated to have {registryModuleCommand[vessel].Count} modules of type {typeof(ModuleCommand).Name}."); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.VesselModuleRegistry]: Specialised registry entry for {vessel.vesselName} updated to have {registryModuleCommand[vessel].Count} modules of type {typeof(ModuleCommand).Name}."); } if (registryKerbalSeat.ContainsKey(vessel) && (partsAdded || registryKerbalSeat[vessel].Count > 0)) { registryKerbalSeat[vessel] = vessel.FindPartModulesImplementing(); - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log($"[BDArmory.VesselModuleRegistry]: Specialised registry entry for {vessel.vesselName} updated to have {registryKerbalSeat[vessel].Count} modules of type {typeof(KerbalSeat).Name}."); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.VesselModuleRegistry]: Specialised registry entry for {vessel.vesselName} updated to have {registryKerbalSeat[vessel].Count} modules of type {typeof(KerbalSeat).Name}."); } if (registryKerbalEVA.ContainsKey(vessel) && (partsAdded || registryKerbalEVA[vessel].Count > 0)) { registryKerbalEVA[vessel] = vessel.FindPartModulesImplementing(); - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log($"[BDArmory.VesselModuleRegistry]: Specialised registry entry for {vessel.vesselName} updated to have {registryKerbalEVA[vessel].Count} modules of type {typeof(KerbalEVA).Name}."); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.VesselModuleRegistry]: Specialised registry entry for {vessel.vesselName} updated to have {registryKerbalEVA[vessel].Count} modules of type {typeof(KerbalEVA).Name}."); + } + if (registryRepulsorModule.ContainsKey(vessel) && (partsAdded || registryRepulsorModule[vessel].Count > 0)) + { + registryRepulsorModule[vessel] = vessel.FindPartModulesImplementing(); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.VesselModuleRegistry]: Specialised registry entry for {vessel.vesselName} updated to have {registryRepulsorModule[vessel].Count} modules of type {typeof(ModuleWheelBase).Name}."); } } #endregion @@ -241,10 +250,10 @@ public static void OnVesselModified(Vessel vessel, bool force = false) /// /// The module type to get. /// The vessel to get the modules from. - /// An enumerable for use in foreach loops or .ToList calls if the vessel exists, else null. + /// An enumerable for use in foreach loops or .ToList. public static List GetModules(Vessel vessel) where T : class { - if (vessel == null || !vessel.loaded) return null; + if (vessel == null || !vessel.loaded) return new List(); // Return empty list. if (typeof(T) == typeof(MissileFire)) { return GetMissileFires(vessel) as List; } if (typeof(T) == typeof(MissileBase)) { return GetMissileBases(vessel) as List; } @@ -258,6 +267,7 @@ public static List GetModules(Vessel vessel) where T : class if (typeof(T) == typeof(ModuleCommand)) { return GetModuleCommands(vessel) as List; } if (typeof(T) == typeof(KerbalSeat)) { return GetKerbalSeats(vessel) as List; } if (typeof(T) == typeof(KerbalEVA)) { return GetKerbalEVAs(vessel) as List; } + if (typeof(T) == typeof(ModuleWheelBase)) { return GetRepulsorModules(vessel) as List; } if (!registry.ContainsKey(vessel)) { Instance.AddVesselToRegistry(vessel); } @@ -306,6 +316,7 @@ public static int GetModuleCount(Vessel vessel) where T : class if (typeof(T) == typeof(ModuleCommand)) { return GetModuleCommands(vessel).Count; } if (typeof(T) == typeof(KerbalSeat)) { return GetKerbalSeats(vessel).Count; } if (typeof(T) == typeof(KerbalEVA)) { return GetKerbalEVAs(vessel).Count; } + if (typeof(T) == typeof(ModuleWheelBase)) { return GetRepulsorModules(vessel).Count; } if (!registry.ContainsKey(vessel) || !registry[vessel].ContainsKey(typeof(T))) { Instance.UpdateVesselModulesInRegistry(vessel); } return registry[vessel][typeof(T)].Count; } @@ -331,6 +342,7 @@ public static void CleanRegistries() registryModuleCommand = registryModuleCommand.Where(kvp => kvp.Key != null && kvp.Value.Count > 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); // Remove null and empty vessel entries. registryKerbalSeat = registryKerbalSeat.Where(kvp => kvp.Key != null && kvp.Value.Count > 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); // Remove null and empty vessel entries. registryKerbalEVA = registryKerbalEVA.Where(kvp => kvp.Key != null && kvp.Value.Count > 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); // Remove null and empty vessel entries. + registryRepulsorModule = registryRepulsorModule.Where(kvp => kvp.Key != null && kvp.Value.Count > 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); // Remove null and empty vessel entries. } #region Specialised methods @@ -340,12 +352,12 @@ public static void CleanRegistries() public static List GetMissileFires(Vessel vessel) { - if (vessel == null || !vessel.loaded) return null; + if (vessel == null || !vessel.loaded) return new List(); if (!registryMissileFire.ContainsKey(vessel)) { registryMissileFire.Add(vessel, vessel.FindPartModulesImplementing()); vesselPartCounts[vessel] = vessel.Parts.Count; - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log($"[BDArmory.VesselModuleRegistry]: Vessel {vessel.vesselName} added to specialised {typeof(MissileFire).Name} registry."); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.VesselModuleRegistry]: Vessel {vessel.vesselName} added to specialised {typeof(MissileFire).Name} registry."); } return registryMissileFire[vessel]; } @@ -364,12 +376,12 @@ public static MissileFire GetMissileFire(Vessel vessel, bool firstNonNull = fals public static List GetMissileBases(Vessel vessel) { - if (vessel == null || !vessel.loaded) return null; + if (vessel == null || !vessel.loaded) return new List(); if (!registryMissileBase.ContainsKey(vessel)) { registryMissileBase.Add(vessel, vessel.FindPartModulesImplementing()); vesselPartCounts[vessel] = vessel.Parts.Count; - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log($"[BDArmory.VesselModuleRegistry]: Vessel {vessel.vesselName} added to specialised {typeof(MissileBase).Name} registry."); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.VesselModuleRegistry]: Vessel {vessel.vesselName} added to specialised {typeof(MissileBase).Name} registry."); } return registryMissileBase[vessel]; } @@ -388,12 +400,12 @@ public static MissileBase GetMissileBase(Vessel vessel, bool firstNonNull = fals public static List GetBDModulePilotAIs(Vessel vessel) { - if (vessel == null || !vessel.loaded) return null; + if (vessel == null || !vessel.loaded) return new List(); if (!registryBDModulePilotAI.ContainsKey(vessel)) { registryBDModulePilotAI.Add(vessel, vessel.FindPartModulesImplementing()); vesselPartCounts[vessel] = vessel.Parts.Count; - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log($"[BDArmory.VesselModuleRegistry]: Vessel {vessel.vesselName} added to specialised {typeof(BDModulePilotAI).Name} registry."); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.VesselModuleRegistry]: Vessel {vessel.vesselName} added to specialised {typeof(BDModulePilotAI).Name} registry."); } return registryBDModulePilotAI[vessel]; } @@ -412,12 +424,12 @@ public static BDModulePilotAI GetBDModulePilotAI(Vessel vessel, bool firstNonNul public static List GetBDModuleSurfaceAIs(Vessel vessel) { - if (vessel == null || !vessel.loaded) return null; + if (vessel == null || !vessel.loaded) return new List(); if (!registryBDModuleSurfaceAI.ContainsKey(vessel)) { registryBDModuleSurfaceAI.Add(vessel, vessel.FindPartModulesImplementing()); vesselPartCounts[vessel] = vessel.Parts.Count; - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log($"[BDArmory.VesselModuleRegistry]: Vessel {vessel.vesselName} added to specialised {typeof(BDModuleSurfaceAI).Name} registry."); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.VesselModuleRegistry]: Vessel {vessel.vesselName} added to specialised {typeof(BDModuleSurfaceAI).Name} registry."); } return registryBDModuleSurfaceAI[vessel]; } @@ -436,12 +448,12 @@ public static BDModuleSurfaceAI GetBDModuleSurfaceAI(Vessel vessel, bool firstNo public static List GetIBDAIControls(Vessel vessel) { - if (vessel == null || !vessel.loaded) return null; + if (vessel == null || !vessel.loaded) return new List(); if (!registryIBDAIControl.ContainsKey(vessel)) { registryIBDAIControl.Add(vessel, vessel.FindPartModulesImplementing()); vesselPartCounts[vessel] = vessel.Parts.Count; - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log($"[BDArmory.VesselModuleRegistry]: Vessel {vessel.vesselName} added to specialised {typeof(IBDAIControl).Name} registry."); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.VesselModuleRegistry]: Vessel {vessel.vesselName} added to specialised {typeof(IBDAIControl).Name} registry."); } return registryIBDAIControl[vessel]; } @@ -460,59 +472,59 @@ public static IBDAIControl GetIBDAIControl(Vessel vessel, bool firstNonNull = fa public static List GetModuleWeapons(Vessel vessel) { - if (vessel == null || !vessel.loaded) return null; + if (vessel == null || !vessel.loaded) return new List(); if (!registryModuleWeapon.ContainsKey(vessel)) { registryModuleWeapon.Add(vessel, vessel.FindPartModulesImplementing()); vesselPartCounts[vessel] = vessel.Parts.Count; - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log($"[BDArmory.VesselModuleRegistry]: Vessel {vessel.vesselName} added to specialised {typeof(ModuleWeapon).Name} registry."); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.VesselModuleRegistry]: Vessel {vessel.vesselName} added to specialised {typeof(ModuleWeapon).Name} registry."); } return registryModuleWeapon[vessel]; } public static List GetIBDWeapons(Vessel vessel) { - if (vessel == null || !vessel.loaded) return null; + if (vessel == null || !vessel.loaded) return new List(); if (!registryIBDWeapon.ContainsKey(vessel)) { registryIBDWeapon.Add(vessel, vessel.FindPartModulesImplementing()); vesselPartCounts[vessel] = vessel.Parts.Count; - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log($"[BDArmory.VesselModuleRegistry]: Vessel {vessel.vesselName} added to specialised {typeof(IBDWeapon).Name} registry."); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.VesselModuleRegistry]: Vessel {vessel.vesselName} added to specialised {typeof(IBDWeapon).Name} registry."); } return registryIBDWeapon[vessel]; } public static List GetModuleEngines(Vessel vessel) { - if (vessel == null || !vessel.loaded) return null; + if (vessel == null || !vessel.loaded) return new List(); if (!registryModuleEngines.ContainsKey(vessel)) { registryModuleEngines.Add(vessel, vessel.FindPartModulesImplementing()); vesselPartCounts[vessel] = vessel.Parts.Count; - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log($"[BDArmory.VesselModuleRegistry]: Vessel {vessel.vesselName} added to specialised {typeof(ModuleEngines).Name} registry."); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.VesselModuleRegistry]: Vessel {vessel.vesselName} added to specialised {typeof(ModuleEngines).Name} registry."); } return registryModuleEngines[vessel]; } - + public static List GetModuleIntakes(Vessel vessel) { - if (vessel == null || !vessel.loaded) return null; + if (vessel == null || !vessel.loaded) return new List(); if (!registryModuleIntakes.ContainsKey(vessel)) { registryModuleIntakes.Add(vessel, vessel.FindPartModulesImplementing()); vesselPartCounts[vessel] = vessel.Parts.Count; - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log($"[BDArmory.VesselModuleRegistry]: Vessel {vessel.vesselName} added to specialised {typeof(ModuleResourceIntake).Name} registry."); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.VesselModuleRegistry]: Vessel {vessel.vesselName} added to specialised {typeof(ModuleResourceIntake).Name} registry."); } return registryModuleIntakes[vessel]; } public static List GetModuleCommands(Vessel vessel) { - if (vessel == null || !vessel.loaded) return null; + if (vessel == null || !vessel.loaded) return new List(); if (!registryModuleCommand.ContainsKey(vessel)) { registryModuleCommand.Add(vessel, vessel.FindPartModulesImplementing()); vesselPartCounts[vessel] = vessel.Parts.Count; - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log($"[BDArmory.VesselModuleRegistry]: Vessel {vessel.vesselName} added to specialised {typeof(ModuleCommand).Name} registry."); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.VesselModuleRegistry]: Vessel {vessel.vesselName} added to specialised {typeof(ModuleCommand).Name} registry."); } return registryModuleCommand[vessel]; } @@ -531,12 +543,12 @@ public static ModuleCommand GetModuleCommand(Vessel vessel, bool firstNonNull = public static List GetKerbalSeats(Vessel vessel) { - if (vessel == null || !vessel.loaded) return null; + if (vessel == null || !vessel.loaded) return new List(); if (!registryKerbalSeat.ContainsKey(vessel)) { registryKerbalSeat.Add(vessel, vessel.FindPartModulesImplementing()); vesselPartCounts[vessel] = vessel.Parts.Count; - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log($"[BDArmory.VesselModuleRegistry]: Vessel {vessel.vesselName} added to specialised {typeof(KerbalSeat).Name} registry."); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.VesselModuleRegistry]: Vessel {vessel.vesselName} added to specialised {typeof(KerbalSeat).Name} registry."); } return registryKerbalSeat[vessel]; } @@ -555,12 +567,12 @@ public static KerbalSeat GetKerbalSeat(Vessel vessel, bool firstNonNull = false) public static List GetKerbalEVAs(Vessel vessel) { - if (vessel == null || !vessel.loaded) return null; + if (vessel == null || !vessel.loaded) return new List(); if (!registryKerbalEVA.ContainsKey(vessel)) { registryKerbalEVA.Add(vessel, vessel.FindPartModulesImplementing()); vesselPartCounts[vessel] = vessel.Parts.Count; - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log($"[BDArmory.VesselModuleRegistry]: Vessel {vessel.vesselName} added to specialised {typeof(KerbalEVA).Name} registry."); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.VesselModuleRegistry]: Vessel {vessel.vesselName} added to specialised {typeof(KerbalEVA).Name} registry."); } return registryKerbalEVA[vessel]; } @@ -576,6 +588,17 @@ public static KerbalEVA GetKerbalEVA(Vessel vessel, bool firstNonNull = false) if (!registryKerbalEVA.ContainsKey(vessel)) { return GetKerbalEVAs(vessel).FirstOrDefault(); } return registryKerbalEVA[vessel].FirstOrDefault(); } + public static List GetRepulsorModules(Vessel vessel) + { + if (vessel == null || !vessel.loaded) return new List(); + if (!registryRepulsorModule.ContainsKey(vessel)) + { + registryRepulsorModule.Add(vessel, vessel.FindPartModulesImplementing()); + vesselPartCounts[vessel] = vessel.Parts.Count; + if (BDArmorySettings.DEBUG_OTHER) Debug.Log($"[BDArmory.VesselModuleRegistry]: Vessel {vessel.vesselName} added to specialised {typeof(ModuleWheelBase).Name} registry."); + } + return registryRepulsorModule[vessel]; + } #endregion #if DEBUG diff --git a/BDArmory/Utils/VesselUtils.cs b/BDArmory/Utils/VesselUtils.cs new file mode 100644 index 000000000..983020518 --- /dev/null +++ b/BDArmory/Utils/VesselUtils.cs @@ -0,0 +1,106 @@ +using System.Collections.Generic; +using UnityEngine; +using KSP.UI.Screens; + +using BDArmory.Control; +using BDArmory.Extensions; +using BDArmory.FX; +using BDArmory.Targeting; + +namespace BDArmory.Utils +{ + public static class VesselUtils + { + // this stupid thing makes all the BD armory parts explode + [KSPField] + private static string explModelPath = "BDArmory/Models/explosion/explosion"; + [KSPField] + public static string explSoundPath = "BDArmory/Sounds/explode1"; + + public static void ForceDeadVessel(Vessel v) + { + Debug.Log("[BDArmory.Misc]: GM Killed Vessel " + v.GetDisplayName()); + foreach (var missileFire in VesselModuleRegistry.GetModules(v)) + { + PartExploderSystem.AddPartToExplode(missileFire.part); + ExplosionFx.CreateExplosion(missileFire.part.transform.position, 1f, explModelPath, explSoundPath, ExplosionSourceType.Other, 0, missileFire.part); + TargetInfo tInfo; + v.vesselType = VesselType.Debris; + if (tInfo = v.gameObject.GetComponent()) + { + UI.BDATargetManager.RemoveTarget(tInfo); //prevent other craft from chasing GM killed craft (in case of maxAltitude or similar + UI.BDATargetManager.LoadedVessels.Remove(v); + } + UI.BDATargetManager.LoadedVessels.RemoveAll(ves => ves == null); + UI.BDATargetManager.LoadedVessels.RemoveAll(ves => ves.loaded == false); + } + } + + + // borrowed from SmartParts - activate the next stage on a vessel + public static void fireNextNonEmptyStage(Vessel v) + { + // the parts to be fired + List resultList = new List(); + + int highestNextStage = getHighestNextStage(v.rootPart, v.currentStage); + traverseChildren(v.rootPart, highestNextStage, ref resultList); + + foreach (Part stageItem in resultList) + { + //Log.Info("Activate:" + stageItem); + stageItem.activate(highestNextStage, stageItem.vessel); + stageItem.inverseStage = v.currentStage; + } + v.currentStage = highestNextStage; + //If this is the currently active vessel, activate the next, now empty, stage. This is an ugly, ugly hack but it's the only way to clear out the empty stage. + //Switching to a vessel that has been staged this way already clears out the empty stage, so this isn't required for those. + if (v.isActiveVessel) + { + StageManager.ActivateNextStage(); + } + } + + private static int getHighestNextStage(Part p, int currentStage) + { + + int highestChildStage = 0; + + // if this is the root part and its a decoupler: ignore it. It was probably fired before. + // This is dirty guesswork but everything else seems not to work. KSP staging is too messy. + if (p.vessel.rootPart == p && + (p.name.IndexOf("ecoupl") != -1 || p.name.IndexOf("eparat") != -1)) + { + } + else if (p.inverseStage < currentStage) + { + highestChildStage = p.inverseStage; + } + + + // Check all children. If this part has no children, inversestage or current Stage will be returned + int childStage = 0; + foreach (Part child in p.children) + { + childStage = getHighestNextStage(child, currentStage); + if (childStage > highestChildStage && childStage < currentStage) + { + highestChildStage = childStage; + } + } + return highestChildStage; + } + + private static void traverseChildren(Part p, int nextStage, ref List resultList) + { + if (p.inverseStage >= nextStage) + { + resultList.Add(p); + } + foreach (Part child in p.children) + { + traverseChildren(child, nextStage, ref resultList); + } + } + } +} \ No newline at end of file diff --git a/BDArmory/VesselSpawning/CircularSpawning.cs b/BDArmory/VesselSpawning/CircularSpawning.cs new file mode 100644 index 000000000..8c28a6c58 --- /dev/null +++ b/BDArmory/VesselSpawning/CircularSpawning.cs @@ -0,0 +1,451 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using UnityEngine; + +using BDArmory.Competition; +using BDArmory.Extensions; +using BDArmory.Settings; +using BDArmory.UI; +using BDArmory.Utils; + +namespace BDArmory.VesselSpawning +{ + /// + /// Spawning of a group of craft in a ring. + /// + /// This is the default spawning code for RWP competitions currently and is essentially what the CircularSpawnStrategy needs to perform before it can take over as the default. + /// + /// TODO: + /// The central block of the SpawnAllVesselsOnce function should eventually switch to using SingleVesselSpawning.Instance.SpawnVessel (plus local coroutines for the extra stuff) to do the actual spawning of the vessels once that's ready. + /// + [KSPAddon(KSPAddon.Startup.Flight, false)] + public class CircularSpawning : VesselSpawnerBase + { + public static CircularSpawning Instance; + protected override void Awake() + { + base.Awake(); + if (Instance != null) Destroy(Instance); + Instance = this; + } + + void LogMessage(string message, bool toScreen = true, bool toLog = true) => LogMessageFrom("CircularSpawning", message, toScreen, toLog); + + public override IEnumerator Spawn(SpawnConfig spawnConfig) + { + var circularSpawnConfig = spawnConfig as CircularSpawnConfig; + if (circularSpawnConfig == null) + { + Debug.LogError($"[BDArmory.CircularSpawning]: SpawnConfig wasn't a valid CircularSpawnConfig"); + yield break; + } + yield return SpawnAllVesselsOnceAsCoroutine(circularSpawnConfig); + } + public void CancelSpawning() + { + // Single spawn + if (vesselsSpawning) + { + vesselsSpawning = false; + LogMessage("Vessel spawning cancelled."); + } + if (spawnAllVesselsOnceCoroutine != null) + { + StopCoroutine(spawnAllVesselsOnceCoroutine); + spawnAllVesselsOnceCoroutine = null; + } + + // Continuous single spawn + if (vesselsSpawningOnceContinuously) + { + vesselsSpawningOnceContinuously = false; + LogMessage("Continuous single spawning cancelled."); + } + if (spawnAllVesselsOnceContinuouslyCoroutine != null) + { + StopCoroutine(spawnAllVesselsOnceContinuouslyCoroutine); + spawnAllVesselsOnceContinuouslyCoroutine = null; + } + } + + #region Single spawning + /// + /// Prespawn initialisation to handle camera and body changes and to ensure that only a single spawning coroutine is running. + /// Note: This currently has some specifics to the SpawnAllVesselsOnceCoroutine, so it may not be suitable for other spawning strategies yet. + /// + /// The spawn config for the new spawning. + public override void PreSpawnInitialisation(SpawnConfig spawnConfig) + { + base.PreSpawnInitialisation(spawnConfig); + + vesselsSpawning = true; // Signal that we've started the spawning vessels routine. + vesselSpawnSuccess = false; // Set our success flag to false for now. + spawnFailureReason = SpawnFailureReason.None; // Reset the spawn failure reason. + if (spawnAllVesselsOnceCoroutine != null) + StopCoroutine(spawnAllVesselsOnceCoroutine); + } + + public void SpawnAllVesselsOnce(int worldIndex, double latitude, double longitude, double altitude = 0, float distance = 10f, bool absDistanceOrFactor = false, bool killEverythingFirst = true, bool assignTeams = true, int numberOfTeams = 0, List teamCounts = null, List> teamsSpecific = null, string spawnFolder = null, List craftFiles = null) + { + SpawnAllVesselsOnce(new CircularSpawnConfig(new SpawnConfig(worldIndex, latitude, longitude, altitude, killEverythingFirst, assignTeams, numberOfTeams, teamCounts, teamsSpecific, spawnFolder, craftFiles), distance, absDistanceOrFactor)); + } + + public void SpawnAllVesselsOnce(CircularSpawnConfig spawnConfig) + { + PreSpawnInitialisation(spawnConfig); + spawnAllVesselsOnceCoroutine = StartCoroutine(SpawnAllVesselsOnceCoroutine(spawnConfig)); + LogMessage("Triggering vessel spawning at " + spawnConfig.latitude.ToString("G6") + ", " + spawnConfig.longitude.ToString("G6") + ", with altitude " + spawnConfig.altitude + "m.", false); + } + + /// + /// A coroutine version of the SpawnAllVesselsOnce function that performs the required prespawn initialisation. + /// + /// The spawn config to use. + public IEnumerator SpawnAllVesselsOnceAsCoroutine(CircularSpawnConfig spawnConfig) + { + PreSpawnInitialisation(spawnConfig); + LogMessage("Triggering vessel spawning at " + spawnConfig.latitude.ToString("G6") + ", " + spawnConfig.longitude.ToString("G6") + ", with altitude " + spawnConfig.altitude + "m.", false); + yield return SpawnAllVesselsOnceCoroutine(spawnConfig); + } + + private Coroutine spawnAllVesselsOnceCoroutine; + // Spawns all vessels in an outward facing ring and lowers them to the ground. An altitude of 5m should be suitable for most cases. + private IEnumerator SpawnAllVesselsOnceCoroutine(CircularSpawnConfig spawnConfig) + { + #region Initialisation and sanity checks + // Tally up the craft to spawn and figure out teams. + if (spawnConfig.teamsSpecific == null) + { + var spawnFolder = Path.Combine(AutoSpawnPath, spawnConfig.folder); + if (!Directory.Exists(spawnFolder)) + { + LogMessage($"Spawn folder {spawnFolder} doesn't exist!"); + vesselsSpawning = false; + spawnFailureReason = SpawnFailureReason.NoCraft; + SpawnUtils.RevertSpawnLocationCamera(true, true); + yield break; + } + if (spawnConfig.numberOfTeams == 1) // Scan subfolders + { + spawnConfig.teamsSpecific = new List>(); + var teamDirs = Directory.GetDirectories(spawnFolder); + if (teamDirs.Length < 2) // Make teams from each vessel in the spawn folder. Allow for a single subfolder for putting bad craft or other tmp things in. + { + spawnConfig.numberOfTeams = -1; // Flag for treating craft files as folder names. + spawnConfig.craftFiles = Directory.GetFiles(spawnFolder).Where(f => f.EndsWith(".craft")).ToList(); + spawnConfig.teamsSpecific = spawnConfig.craftFiles.Select(f => new List { f }).ToList(); + } + else + { + LogMessage("Spawning teams from folders " + string.Join(", ", teamDirs.Select(d => d.Substring(AutoSpawnPath.Length))), false); + foreach (var teamDir in teamDirs) + { + spawnConfig.teamsSpecific.Add(Directory.GetFiles(teamDir, "*.craft").ToList()); + } + spawnConfig.craftFiles = spawnConfig.teamsSpecific.SelectMany(v => v.ToList()).ToList(); + } + } + else // Just the specified folder. + { + if (spawnConfig.craftFiles == null) // Prioritise the list of craftFiles if we're given them. + spawnConfig.craftFiles = Directory.GetFiles(spawnFolder).Where(f => f.EndsWith(".craft")).ToList(); + } + } + else // Spawn the specific vessels. + { + spawnConfig.craftFiles = spawnConfig.teamsSpecific.SelectMany(v => v.ToList()).ToList(); + } + if (spawnConfig.craftFiles.Count == 0) + { + LogMessage("Vessel spawning: found no craft files in " + Path.Combine(AutoSpawnPath, spawnConfig.folder)); + vesselsSpawning = false; + spawnFailureReason = SpawnFailureReason.NoCraft; + SpawnUtils.RevertSpawnLocationCamera(true, true); + yield break; + } + bool useOriginalTeamNames = spawnConfig.assignTeams && (spawnConfig.numberOfTeams == 1 || spawnConfig.numberOfTeams == -1); // We'll be using the folders or craft filenames as team names in the originalTeams dictionary. + if (spawnConfig.teamsSpecific != null && !useOriginalTeamNames) + { + spawnConfig.teamCounts = spawnConfig.teamsSpecific.Select(tl => tl.Count).ToList(); + } + if (BDArmorySettings.VESSEL_SPAWN_RANDOM_ORDER) spawnConfig.craftFiles.Shuffle(); // Randomise the spawn order. + int spawnedVesselCount = 0; // Reset our spawned vessel count. + var spawnAirborne = spawnConfig.altitude > 10; + bool PinataMode = false; + foreach (var craftUrl in spawnConfig.craftFiles) + { + if (!String.IsNullOrEmpty(BDArmorySettings.PINATA_NAME) && craftUrl.Contains(BDArmorySettings.PINATA_NAME)) PinataMode = true; + } + var spawnDistance = spawnConfig.craftFiles.Count > 1 ? (spawnConfig.absDistanceOrFactor ? spawnConfig.distance : (spawnConfig.distance + spawnConfig.distance * (spawnConfig.craftFiles.Count - (PinataMode ? 1 : 0)))) : 0f; // If it's a single craft, spawn it at the spawn point. + + LogMessage("Spawning " + (spawnConfig.craftFiles.Count - (PinataMode ? 1 : 0)) + " vessels at an altitude of " + spawnConfig.altitude.ToString("G0") + "m" + (spawnConfig.craftFiles.Count > 8 ? ", this may take some time..." : ".")); + #endregion + + yield return AcquireSpawnPoint(spawnConfig, 2f * spawnDistance, spawnAirborne); + if (spawnFailureReason != SpawnFailureReason.None) + { + vesselsSpawning = false; + SpawnUtils.RevertSpawnLocationCamera(true, true); + yield break; + } + + #region Spawn layout configuration + // Spawn the craft in an outward facing ring. If using teams, cluster the craft around each team spawn point. + var radialUnitVector = (spawnPoint - FlightGlobals.currentMainBody.transform.position).normalized; + var refDirection = Math.Abs(Vector3.Dot(Vector3.up, radialUnitVector)) < 0.71f ? Vector3.up : Vector3.forward; // Avoid that the reference direction is colinear with the local surface normal. + var vesselSpawnConfigs = new List(); + if (spawnConfig.teamsSpecific == null) + { + foreach (var craftUrl in spawnConfig.craftFiles) + { + // Figure out spawn point and orientation + var heading = 360f * spawnedVesselCount / spawnConfig.craftFiles.Count - (PinataMode ? 1 : 0); + var direction = (Quaternion.AngleAxis(heading, radialUnitVector) * refDirection).ProjectOnPlanePreNormalized(radialUnitVector).normalized; + Vector3 position = spawnPoint; + if (!PinataMode || (PinataMode && !craftUrl.Contains(BDArmorySettings.PINATA_NAME)))//leave pinata craft at center + { + position = spawnPoint + spawnDistance * direction; + ++spawnedVesselCount; + } + if (spawnDistance > BDArmorySettings.COMPETITION_DISTANCE / 2f / Mathf.Sin(Mathf.PI / spawnConfig.craftFiles.Count)) direction *= -1f; //have vessels spawning further than comp dist spawn pointing inwards instead of outwards + vesselSpawnConfigs.Add(new VesselSpawnConfig(craftUrl, position, direction, (float)spawnConfig.altitude, -80f, spawnAirborne)); + } + } + else + { + if (BDArmorySettings.VESSEL_SPAWN_RANDOM_ORDER) spawnConfig.teamsSpecific.Shuffle(); // Randomise the team spawn order. + int spawnedTeamCount = 0; + Vector3 teamSpawnPosition; + foreach (var team in spawnConfig.teamsSpecific) + { + if (BDArmorySettings.VESSEL_SPAWN_RANDOM_ORDER) team.Shuffle(); // Randomise the spawn order within the team. + var teamHeading = 360f * spawnedTeamCount / spawnConfig.teamsSpecific.Count; + var teamDirection = (Quaternion.AngleAxis(teamHeading, radialUnitVector) * refDirection).ProjectOnPlanePreNormalized(radialUnitVector).normalized; + teamSpawnPosition = spawnPoint + spawnDistance * teamDirection; + int teamSpawnCount = 0; + float intraTeamSeparation = Mathf.Min(20f * Mathf.Log10(spawnDistance), 4f * BDAMath.Sqrt(spawnDistance)); + var spreadDirection = Vector3.Cross(radialUnitVector, teamDirection); + var facingDirection = (spawnDistance > BDArmorySettings.COMPETITION_DISTANCE / 2f / Mathf.Sin(Mathf.PI / spawnConfig.teamsSpecific.Count)) ? -teamDirection : teamDirection; // Spawn facing inwards if competition distance is closer than spawning distance. + + foreach (var craftUrl in team) + { + // Figure out spawn point and orientation + var position = teamSpawnPosition + intraTeamSeparation * (teamSpawnCount - (team.Count - 1) / 2) * spreadDirection; + vesselSpawnConfigs.Add(new VesselSpawnConfig(craftUrl, position, facingDirection, (float)spawnConfig.altitude, -80f, spawnAirborne)); + ++spawnedVesselCount; + ++teamSpawnCount; + } + ++spawnedTeamCount; + } + } + #endregion + + yield return SpawnVessels(vesselSpawnConfigs); + if (spawnFailureReason != SpawnFailureReason.None) + { + vesselsSpawning = false; + SpawnUtils.RevertSpawnLocationCamera(true, true); + yield break; + } + + #region Post-spawning + // Spawning has succeeded, vessels have been renamed where necessary and vessels are ready. Time to assign teams and any other stuff. + List> teamVesselNames = null; + if (spawnConfig.teamsSpecific != null) + { + if (spawnConfig.assignTeams) // Configure team names. We'll do the actual team assignment later. + { + switch (spawnConfig.numberOfTeams) + { + case 1: // Assign team names based on folders. + { + foreach (var vesselName in spawnedVesselURLs.Keys) + SpawnUtils.originalTeams[vesselName] = Path.GetFileName(Path.GetDirectoryName(spawnedVesselURLs[vesselName])); + break; + } + case -1: // Assign team names based on craft filename. We can't use vessel name as that can get adjusted above to avoid conflicts. + { + foreach (var vesselName in spawnedVesselURLs.Keys) + SpawnUtils.originalTeams[vesselName] = Path.GetFileNameWithoutExtension(spawnedVesselURLs[vesselName]); + break; + } + default: // Specific team assignments. + { + teamVesselNames = new List>(); + for (int i = 0; i < spawnedVesselsTeamIndex.Max(kvp => kvp.Value); ++i) + teamVesselNames.Add(spawnedVesselsTeamIndex.Where(kvp => kvp.Value == i).Select(kvp => kvp.Key).ToList()); + break; + } + } + } + } + + yield return PostSpawnMainSequence(spawnConfig, spawnAirborne); + if (spawnFailureReason != SpawnFailureReason.None) + { + LogMessage("Vessel spawning FAILED! " + spawnFailureReason); + vesselsSpawning = false; + SpawnUtils.RevertSpawnLocationCamera(true, true); + yield break; + } + + // Revert the camera and focus on one of the vessels. + SpawnUtils.RevertSpawnLocationCamera(true); + if ((FlightGlobals.ActiveVessel == null || FlightGlobals.ActiveVessel.state == Vessel.State.DEAD) && spawnedVessels.Count > 0) + { + LoadedVesselSwitcher.Instance.ForceSwitchVessel(spawnedVessels.Take(UnityEngine.Random.Range(1, spawnedVessels.Count)).Last().Value); // Update the camera. + } + FlightCamera.fetch.SetDistance(50); + + if (spawnConfig.assignTeams) + { + // Assign the vessels to teams. + LogMessage("Assigning vessels to teams.", false); + if (spawnConfig.teamsSpecific == null && spawnConfig.teamCounts == null && spawnConfig.numberOfTeams > 1) + { + int numberPerTeam = spawnedVessels.Count / spawnConfig.numberOfTeams; + int residue = spawnedVessels.Count - numberPerTeam * spawnConfig.numberOfTeams; + spawnConfig.teamCounts = new List(); + for (int team = 0; team < spawnConfig.numberOfTeams; ++team) + spawnConfig.teamCounts.Add(numberPerTeam + (team < residue ? 1 : 0)); + } + LoadedVesselSwitcher.Instance.MassTeamSwitch(true, useOriginalTeamNames, spawnConfig.teamCounts, teamVesselNames); + } + #endregion + + LogMessage("Vessel spawning SUCCEEDED!", true, BDArmorySettings.DEBUG_SPAWNING); + vesselSpawnSuccess = true; + vesselsSpawning = false; + } + #endregion + + // TODO Continuous Single Spawning and Team Spawning should probably, at some point, be separated into their own spawn strategies that make use of the above spawning functions. + #region Continuous Single Spawning + public bool vesselsSpawningOnceContinuously = false; + public Coroutine spawnAllVesselsOnceContinuouslyCoroutine = null; + + public void SpawnAllVesselsOnceContinuously(int worldIndex, double latitude, double longitude, double altitude = 0, float distance = 10f, bool absDistanceOrFactor = false, bool killEverythingFirst = true, bool assignTeams = true, int numberOfTeams = 0, List teamCounts = null, List> teamsSpecific = null, string spawnFolder = null, List craftFiles = null) + { + SpawnAllVesselsOnceContinuously(new CircularSpawnConfig(new SpawnConfig(worldIndex, latitude, longitude, altitude, killEverythingFirst, assignTeams, numberOfTeams, teamCounts, teamsSpecific, spawnFolder, craftFiles), distance, absDistanceOrFactor)); + } + public void SpawnAllVesselsOnceContinuously(CircularSpawnConfig spawnConfig) + { + vesselsSpawningOnceContinuously = true; + if (spawnAllVesselsOnceContinuouslyCoroutine != null) + StopCoroutine(spawnAllVesselsOnceContinuouslyCoroutine); + spawnAllVesselsOnceContinuouslyCoroutine = StartCoroutine(SpawnAllVesselsOnceContinuouslyCoroutine(spawnConfig)); + LogMessage("Triggering vessel spawning (continuous single) at " + spawnConfig.latitude.ToString("G6") + ", " + spawnConfig.longitude.ToString("G6") + ", with altitude " + spawnConfig.altitude + "m.", false); + } + + public IEnumerator SpawnAllVesselsOnceContinuouslyCoroutine(CircularSpawnConfig spawnConfig) + { + while (vesselsSpawningOnceContinuously && BDArmorySettings.VESSEL_SPAWN_CONTINUE_SINGLE_SPAWNING) + { + SpawnAllVesselsOnce(spawnConfig); + while (vesselsSpawning) + yield return waitForFixedUpdate; + if (!vesselSpawnSuccess) + { + vesselsSpawningOnceContinuously = false; + yield break; + } + yield return waitForFixedUpdate; + + // NOTE: runs in separate coroutine + BDACompetitionMode.Instance.StartCompetitionMode(BDArmorySettings.COMPETITION_DISTANCE, BDArmorySettings.COMPETITION_START_DESPITE_FAILURES); + yield return waitForFixedUpdate; // Give the competition start a frame to get going. + + // start timer coroutine for the duration specified in settings UI + var duration = BDArmorySettings.COMPETITION_DURATION * 60f; + LogMessage("Starting " + (duration > 0 ? "a " + duration.ToString("F0") + "s" : "an unlimited") + " duration competition."); + while (BDACompetitionMode.Instance.competitionStarting) + yield return waitForFixedUpdate; // Wait for the competition to actually start. + if (!BDACompetitionMode.Instance.competitionIsActive) + { + LogMessage("Competition failed to start."); + vesselsSpawningOnceContinuously = false; + yield break; + } + while (BDACompetitionMode.Instance.competitionIsActive) // Wait for the competition to finish (limited duration and log dumping is handled directly by the competition now). + yield return new WaitForSeconds(1); + + // Wait 10s for any user action + double startTime = Planetarium.GetUniversalTime(); + if (BDArmorySettings.VESSEL_SPAWN_CONTINUE_SINGLE_SPAWNING) + { + while (vesselsSpawningOnceContinuously && Planetarium.GetUniversalTime() - startTime < BDArmorySettings.TOURNAMENT_DELAY_BETWEEN_HEATS) + { + LogMessage("Waiting " + (BDArmorySettings.TOURNAMENT_DELAY_BETWEEN_HEATS - (Planetarium.GetUniversalTime() - startTime)).ToString("0") + "s, then respawning pilots", true, false); + yield return new WaitForSeconds(1); + } + } + } + vesselsSpawningOnceContinuously = false; // For when VESSEL_SPAWN_CONTINUE_SINGLE_SPAWNING gets toggled. + } + #endregion + + #region Team Spawning + /// + /// Spawn multiple groups of vessels using the CircularSpawning using multiple SpawnConfigs. + /// + /// + /// + /// + /// + public void TeamSpawn(List spawnConfigs, bool startCompetition = false, double competitionStartDelay = 0d, bool startCompetitionNow = false) + { + vesselsSpawning = true; // Indicate that vessels are spawning here to avoid timing issues with Update in other modules. + SpawnUtils.RevertSpawnLocationCamera(true); + if (teamSpawnCoroutine != null) + StopCoroutine(teamSpawnCoroutine); + teamSpawnCoroutine = StartCoroutine(TeamsSpawnCoroutine(spawnConfigs, startCompetition, competitionStartDelay, startCompetitionNow)); + } + private Coroutine teamSpawnCoroutine; + public IEnumerator TeamsSpawnCoroutine(List spawnConfigs, bool startCompetition = false, double competitionStartDelay = 0d, bool startCompetitionNow = false) + { + bool killAllFirst = true; + List spawnCounts = new List(); + spawnFailureReason = SpawnFailureReason.None; + // Spawn each team. + foreach (var spawnConfig in spawnConfigs) + { + vesselsSpawning = true; // Gets set to false each time spawning is finished, so we need to re-enable it again. + vesselSpawnSuccess = false; + spawnConfig.killEverythingFirst = killAllFirst; + yield return SpawnAllVesselsOnceCoroutine(spawnConfig); + if (!vesselSpawnSuccess) + { + LogMessage("Vessel spawning failed, aborting."); + yield break; + } + spawnCounts.Add(spawnedVessels.Count); + // LoadedVesselSwitcher.Instance.MassTeamSwitch(false); // Reset everyone to team 'A' so that the order doesn't get messed up. + killAllFirst = false; + } + yield return waitForFixedUpdate; + SpawnUtils.SaveTeams(); // Save the teams in case they've been pre-configured. + LoadedVesselSwitcher.Instance.MassTeamSwitch(false, false, spawnCounts); // Assign teams based on the groups of spawns. Right click the 'T' to revert to the original team names if they were defined. + if (startCompetition) // Start the competition. + { + var competitionStartDelayStart = Planetarium.GetUniversalTime(); + while (Planetarium.GetUniversalTime() - competitionStartDelayStart < competitionStartDelay - Time.fixedDeltaTime) + { + var timeLeft = competitionStartDelay - (Planetarium.GetUniversalTime() - competitionStartDelayStart); + if ((int)(timeLeft - Time.fixedDeltaTime) < (int)timeLeft) + LogMessage("Competition starting in T-" + timeLeft.ToString("0") + "s", true, false); + yield return waitForFixedUpdate; + } + BDACompetitionMode.Instance.StartCompetitionMode(BDArmorySettings.COMPETITION_DISTANCE, BDArmorySettings.COMPETITION_START_DESPITE_FAILURES); + if (startCompetitionNow) + { + yield return waitForFixedUpdate; + BDACompetitionMode.Instance.StartCompetitionNow(); + } + } + } + #endregion + } +} diff --git a/BDArmory/VesselSpawning/ContinuousSpawning.cs b/BDArmory/VesselSpawning/ContinuousSpawning.cs new file mode 100644 index 000000000..d0f81b83c --- /dev/null +++ b/BDArmory/VesselSpawning/ContinuousSpawning.cs @@ -0,0 +1,425 @@ +using UnityEngine; +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +using BDArmory.Competition; +using BDArmory.Control; +using BDArmory.Extensions; +using BDArmory.Settings; +using BDArmory.Utils; +using BDArmory.UI; + +namespace BDArmory.VesselSpawning +{ + /// + /// Continous spawning in an airborne ring cycling through all the vessels in the spawn folder. + /// + /// TODO: This should probably be subsumed into its own spawn strategy eventually. + /// + [KSPAddon(KSPAddon.Startup.Flight, false)] + public class ContinuousSpawning : VesselSpawnerBase + { + public static ContinuousSpawning Instance; + + public bool vesselsSpawningContinuously = false; + int continuousSpawnedVesselCount = 0; + int currentlySpawningCount = 0; + + protected override void Awake() + { + base.Awake(); + if (Instance != null) Destroy(Instance); + Instance = this; + } + + void LogMessage(string message, bool toScreen = true, bool toLog = true) => LogMessageFrom("ContinuousSpawning", message, toScreen, toLog); + + public override IEnumerator Spawn(SpawnConfig spawnConfig) + { + var circularSpawnConfig = spawnConfig as CircularSpawnConfig; + if (circularSpawnConfig == null) yield break; + yield return SpawnVesselsContinuouslyAsCoroutine(circularSpawnConfig); + } + + public void CancelSpawning() + { + // Continuous spawn + if (vesselsSpawningContinuously) + { + vesselsSpawningContinuously = false; + if (continuousSpawningScores != null) + DumpContinuousSpawningScores(); + continuousSpawningScores = null; + LogMessage("Continuous vessel spawning cancelled."); + if (BDACompetitionMode.Instance != null) BDACompetitionMode.Instance.ResetCompetitionStuff(); + } + currentlySpawningCount = 0; + spawnVesselsContinuouslyCoroutine = null; + } + + public override void PreSpawnInitialisation(SpawnConfig spawnConfig) + { + base.PreSpawnInitialisation(spawnConfig); + + vesselsSpawningContinuously = true; + spawnFailureReason = SpawnFailureReason.None; // Reset the spawn failure reason. + continuousSpawningScores = new Dictionary(); + if (spawnVesselsContinuouslyCoroutine != null) + StopCoroutine(spawnVesselsContinuouslyCoroutine); + // Reset competition stuff. + BDACompetitionMode.Instance.LogResults("due to continuous spawning", "auto-dump-from-spawning"); // Log results first. + BDACompetitionMode.Instance.StopCompetition(); + BDACompetitionMode.Instance.ResetCompetitionStuff(); // Reset competition scores. + } + + public void SpawnVesselsContinuously(CircularSpawnConfig spawnConfig) + { + PreSpawnInitialisation(spawnConfig); + LogMessage("[BDArmory.VesselSpawner]: Triggering continuous vessel spawning at " + spawnConfig.latitude.ToString("G6") + ", " + spawnConfig.longitude.ToString("G6") + ", with altitude " + spawnConfig.altitude + "m.", false); + spawnVesselsContinuouslyCoroutine = StartCoroutine(SpawnVesselsContinuouslyCoroutine(spawnConfig)); + } + + /// + /// A coroutine version of the SpawnAllVesselsContinuously function that performs the required prespawn initialisation. + /// + /// The spawn config to use. + public IEnumerator SpawnVesselsContinuouslyAsCoroutine(CircularSpawnConfig spawnConfig) + { + PreSpawnInitialisation(spawnConfig); + LogMessage("[BDArmory.VesselSpawner]: Triggering continuous vessel spawning at " + spawnConfig.latitude.ToString("G6") + ", " + spawnConfig.longitude.ToString("G6") + ", with altitude " + spawnConfig.altitude + "m.", false); + yield return SpawnVesselsContinuouslyCoroutine(spawnConfig); + } + + private Coroutine spawnVesselsContinuouslyCoroutine; + // HashSet vesselsToActivate = new HashSet(); + // Spawns all vessels in a downward facing ring and activates them (autopilot and AG10, then stage if no engines are firing), then respawns any that die. An altitude of 1000m should be plenty. + // Note: initial vessel separation tends towards 2*pi*spawnDistanceFactor from above for >3 vessels. + private IEnumerator SpawnVesselsContinuouslyCoroutine(CircularSpawnConfig spawnConfig) + { + #region Initialisation and sanity checks + // Tally up the craft to spawn. + if (spawnConfig.craftFiles == null) // Prioritise the list of craftFiles if we're given them. + spawnConfig.craftFiles = Directory.GetFiles(Path.Combine(AutoSpawnPath, spawnConfig.folder), "*.craft").ToList(); + if (spawnConfig.craftFiles.Count == 0) + { + LogMessage("Vessel spawning: found no craft files in " + Path.Combine(AutoSpawnPath, spawnConfig.folder)); + vesselsSpawningContinuously = false; + spawnFailureReason = SpawnFailureReason.NoCraft; + yield break; + } + spawnConfig.craftFiles.Shuffle(); // Randomise the spawn order. + spawnConfig.altitude = Math.Max(100, spawnConfig.altitude); // Don't spawn too low. + var spawnDistance = spawnConfig.craftFiles.Count > 1 ? (spawnConfig.absDistanceOrFactor ? spawnConfig.distance : spawnConfig.distance * (1 + (BDArmorySettings.VESSEL_SPAWN_CONCURRENT_VESSELS > 0 ? Math.Min(spawnConfig.craftFiles.Count, BDArmorySettings.VESSEL_SPAWN_CONCURRENT_VESSELS) : spawnConfig.craftFiles.Count))) : 0f; // If it's a single craft, spawn it at the spawn point. + if (BDArmorySettings.VESSEL_SPAWN_CONCURRENT_VESSELS == 0) + LogMessage("Spawning " + spawnConfig.craftFiles.Count + " vessels at an altitude of " + spawnConfig.altitude.ToString("G0") + (spawnConfig.craftFiles.Count > 8 ? "m, this may take some time..." : "m.")); + else + LogMessage("Spawning " + Math.Min(BDArmorySettings.VESSEL_SPAWN_CONCURRENT_VESSELS, spawnConfig.craftFiles.Count) + " of " + spawnConfig.craftFiles.Count + " vessels at an altitude of " + spawnConfig.altitude.ToString("G0") + "m with rolling-spawning."); + #endregion + + yield return AcquireSpawnPoint(spawnConfig, 2f * spawnDistance, true); + if (spawnFailureReason != SpawnFailureReason.None) + { + vesselsSpawningContinuously = false; + yield break; + } + + #region Spawning + ResetInternals(); + continuousSpawnedVesselCount = 0; // Reset our spawned vessel count. + // vesselsToActivate.Clear(); // Clear any pending vessel activations. + // Get the spawning point in world position coordinates. + var terrainAltitude = FlightGlobals.currentMainBody.TerrainAltitude(spawnConfig.latitude, spawnConfig.longitude); + var spawnPoint = FlightGlobals.currentMainBody.GetWorldSurfacePosition(spawnConfig.latitude, spawnConfig.longitude, terrainAltitude + spawnConfig.altitude); + var radialUnitVector = (spawnPoint - FlightGlobals.currentMainBody.transform.position).normalized; + + var craftURLToVesselName = new Dictionary(); + Vector3d craftGeoCoords; + Vector3 craftSpawnPosition; + var refDirection = Math.Abs(Vector3.Dot(Vector3.up, radialUnitVector)) < 0.71f ? Vector3.up : Vector3.forward; // Avoid that the reference direction is colinear with the local surface normal. + var geeDirection = FlightGlobals.getGeeForceAtPosition(Vector3.zero); + var spawnSlots = OptimiseSpawnSlots(BDArmorySettings.VESSEL_SPAWN_CONCURRENT_VESSELS > 0 ? Math.Min(spawnConfig.craftFiles.Count, BDArmorySettings.VESSEL_SPAWN_CONCURRENT_VESSELS) : spawnConfig.craftFiles.Count); + var spawnCounts = spawnConfig.craftFiles.ToDictionary(c => c, c => 0); + var spawnQueue = new Queue(); + var craftToSpawn = new Queue(); + double currentUpdateTick; + while (vesselsSpawningContinuously) + { + // Wait for any pending vessel removals. + while (SpawnUtils.removingVessels) + { yield return waitForFixedUpdate; } + + currentUpdateTick = BDACompetitionMode.Instance.nextUpdateTick; + if (currentlySpawningCount == 0) // Do nothing while we're spawning vessels. + { + // Reacquire the spawn point as the local coordinate system may have changed (floating origin adjustments, local body rotation, etc.). + spawnPoint = FlightGlobals.currentMainBody.GetWorldSurfacePosition(spawnConfig.latitude, spawnConfig.longitude, terrainAltitude + spawnConfig.altitude); + radialUnitVector = (spawnPoint - FlightGlobals.currentMainBody.transform.position).normalized; + // Check if sliders have changed. + if (spawnSlots.Count != (BDArmorySettings.VESSEL_SPAWN_CONCURRENT_VESSELS > 0 ? Math.Min(spawnConfig.craftFiles.Count, BDArmorySettings.VESSEL_SPAWN_CONCURRENT_VESSELS) : spawnConfig.craftFiles.Count)) + { + spawnSlots = OptimiseSpawnSlots(BDArmorySettings.VESSEL_SPAWN_CONCURRENT_VESSELS > 0 ? Math.Min(spawnConfig.craftFiles.Count, BDArmorySettings.VESSEL_SPAWN_CONCURRENT_VESSELS) : spawnConfig.craftFiles.Count); + continuousSpawnedVesselCount %= spawnSlots.Count; + } + // Add any craft that hasn't been spawned or has died to the spawn queue if it isn't already in the queue. + foreach (var craftURL in spawnConfig.craftFiles.Where(craftURL => (BDArmorySettings.VESSEL_SPAWN_LIVES_PER_VESSEL > 0 ? spawnCounts[craftURL] < BDArmorySettings.VESSEL_SPAWN_LIVES_PER_VESSEL : true) && !spawnQueue.Contains(craftURL) && (!craftURLToVesselName.ContainsKey(craftURL) || (BDACompetitionMode.Instance.Scores.Players.Contains(craftURLToVesselName[craftURL]) && BDACompetitionMode.Instance.Scores.ScoreData[craftURLToVesselName[craftURL]].deathTime >= 0)))) + { + if (BDArmorySettings.DEBUG_SPAWNING) + { + LogMessage($"Adding {craftURL}" + (craftURLToVesselName.ContainsKey(craftURL) ? $" ({craftURLToVesselName[craftURL]})" : "") + " to the spawn queue.", false); + } + spawnQueue.Enqueue(craftURL); + ++spawnCounts[craftURL]; + } + LoadedVesselSwitcher.Instance.UpdateList(); + var currentlyActive = LoadedVesselSwitcher.Instance.WeaponManagers.SelectMany(tm => tm.Value).ToList().Count; + if (spawnQueue.Count + currentlySpawningCount == 0 && currentlyActive < 2)// Nothing left to spawn or being spawned and only 1 vessel surviving. Time to call it quits and let the competition end. + { + LogMessage("Spawn queue is empty and not enough vessels are active, ending competition.", false); + BDACompetitionMode.Instance.StopCompetition(); + break; + } + while (craftToSpawn.Count + currentlySpawningCount + currentlyActive < spawnSlots.Count && spawnQueue.Count > 0) + craftToSpawn.Enqueue(spawnQueue.Dequeue()); + if (BDArmorySettings.DEBUG_SPAWNING) + { + var missing = spawnConfig.craftFiles.Where(craftURL => craftURLToVesselName.ContainsKey(craftURL) && (!spawnCounts.ContainsKey(craftURL) || spawnCounts[craftURL] < BDArmorySettings.VESSEL_SPAWN_LIVES_PER_VESSEL) && !craftToSpawn.Contains(craftURL) && !FlightGlobals.Vessels.Where(v => !VesselModuleRegistry.ignoredVesselTypes.Contains(v.vesselType) && VesselModuleRegistry.GetModuleCount(v) > 0).Select(v => v.vesselName).Contains(craftURLToVesselName[craftURL])).ToList(); + if (missing.Count > 0) + { + LogMessage("MISSING vessels: " + string.Join(", ", craftURLToVesselName.Where(c => missing.Contains(c.Key)).Select(c => c.Value)), false); + } + } + if (craftToSpawn.Count > 0) + { + VesselModuleRegistry.CleanRegistries(); // Clean out any old entries. + // Configure vessel spawn configs + foreach (var craftURL in craftToSpawn) + { + if (BDArmorySettings.DEBUG_SPAWNING) LogMessage($"Spawning vessel from {Path.Combine(AutoSpawnFolder, craftURL.Substring(AutoSpawnPath.Length))}", false); + var heading = 360f * spawnSlots[continuousSpawnedVesselCount] / spawnSlots.Count; + ++continuousSpawnedVesselCount; + continuousSpawnedVesselCount %= spawnSlots.Count; + var direction = (Quaternion.AngleAxis(heading, radialUnitVector) * refDirection).ProjectOnPlanePreNormalized(radialUnitVector).normalized; + craftSpawnPosition = spawnPoint + spawnDistance * direction; + FlightGlobals.currentMainBody.GetLatLonAlt(craftSpawnPosition, out craftGeoCoords.x, out craftGeoCoords.y, out craftGeoCoords.z); // Convert spawn point to geo-coords for the actual spawning function. + StartCoroutine(SpawnCraft(new VesselSpawnConfig(craftURL, craftSpawnPosition, direction, (float)spawnConfig.altitude, -80f, true, 0, true))); + } + craftURLToVesselName = spawnedVesselURLs.ToDictionary(kvp => kvp.Value, kvp => kvp.Key); // Update the vesselName-to-craftURL dictionary for the latest spawns. + craftToSpawn.Clear(); // Clear the queue since we just spawned all those vessels. + } + + // Start the competition once we have enough craft. + if (currentlyActive > 1 && !(BDACompetitionMode.Instance.competitionIsActive || BDACompetitionMode.Instance.competitionStarting)) + { BDACompetitionMode.Instance.StartCompetitionMode(BDArmorySettings.COMPETITION_DISTANCE, BDArmorySettings.COMPETITION_START_DESPITE_FAILURES); } + } + + // Kill off vessels that are out of ammo for too long if we're in continuous spawning mode and a competition is active. + if (BDACompetitionMode.Instance.competitionIsActive) + KillOffOutOfAmmoVessels(); + + if (BDACompetitionMode.Instance.competitionIsActive) + { + yield return new WaitUntil(() => Planetarium.GetUniversalTime() > currentUpdateTick); // Wait for the current update tick in BDACompetitionMode so that spawning occurs after checks for dead vessels there. + yield return waitForFixedUpdate; + } + else + { + yield return new WaitForSeconds(1); // 1s between checks. Nothing much happens if nothing needs spawning. + } + } + #endregion + vesselsSpawningContinuously = false; + LogMessage("[BDArmory.VesselSpawner]: Continuous vessel spawning ended.", false); + } + + IEnumerator SpawnCraft(VesselSpawnConfig vesselSpawnConfig) + { + ++currentlySpawningCount; + // Spawn vessel + yield return SpawnSingleVessel(vesselSpawnConfig); + if (spawnFailureReason != SpawnFailureReason.None) yield break; + var vessel = GetSpawnedVesselsName(vesselSpawnConfig.craftURL); + if (vessel == null) + { + --currentlySpawningCount; + yield break; + } + + // Perform post-spawn stuff. + yield return PostSpawnMainSequence(vessel, true); + if (spawnFailureReason != SpawnFailureReason.None) + { + --currentlySpawningCount; + yield break; + } + + // Spawning went fine. Time to blow stuff up! + AddToActiveCompetition(vessel, true); + + --currentlySpawningCount; + } + + // Stagger the spawn slots to avoid consecutive craft being launched too close together. + private List OptimiseSpawnSlots(int slotCount) + { + var availableSlots = Enumerable.Range(0, slotCount).ToList(); + if (slotCount < 4) return availableSlots; // Can't do anything about it for < 4 craft. + var separation = Mathf.CeilToInt(slotCount / 3f); // Start with approximately 120° separation. + var pos = 0; + var optimisedSlots = new List(); + while (optimisedSlots.Count < slotCount) + { + while (optimisedSlots.Contains(pos)) { ++pos; pos %= slotCount; } + optimisedSlots.Add(pos); + pos += separation; + pos %= slotCount; + } + return optimisedSlots; + } + + #region Scoring + // For tracking scores across multiple spawns. + public class ContinuousSpawningScores + { + public Vessel vessel; // The vessel. + public int spawnCount = 0; // The number of times a craft has been spawned. + public double outOfAmmoTime = 0; // The time the vessel ran out of ammo. + public Dictionary scoreData = new Dictionary(); + public double cumulativeTagTime = 0; + public int cumulativeHits = 0; + public int cumulativeDamagedPartsDueToRamming = 0; + public int cumulativeDamagedPartsDueToRockets = 0; + public int cumulativeDamagedPartsDueToMissiles = 0; + }; + public Dictionary continuousSpawningScores; + public void UpdateCompetitionScores(Vessel vessel, bool newSpawn = false) + { + var vesselName = vessel.vesselName; + if (!continuousSpawningScores.ContainsKey(vesselName)) return; + var spawnCount = continuousSpawningScores[vesselName].spawnCount - 1; + if (spawnCount < 0) return; // Initial spawning after scores were reset. + var scoreData = continuousSpawningScores[vesselName].scoreData; + if (BDACompetitionMode.Instance.Scores.Players.Contains(vesselName)) + { + scoreData[spawnCount] = BDACompetitionMode.Instance.Scores.ScoreData[vesselName]; // Save the Score instance for the vessel. + if (newSpawn) + { + continuousSpawningScores[vesselName].cumulativeTagTime = scoreData.Sum(kvp => kvp.Value.tagTotalTime); + continuousSpawningScores[vesselName].cumulativeHits = scoreData.Sum(kvp => kvp.Value.hits); + continuousSpawningScores[vesselName].cumulativeDamagedPartsDueToRamming = scoreData.Sum(kvp => kvp.Value.totalDamagedPartsDueToRamming); + continuousSpawningScores[vesselName].cumulativeDamagedPartsDueToRockets = scoreData.Sum(kvp => kvp.Value.totalDamagedPartsDueToRockets); + continuousSpawningScores[vesselName].cumulativeDamagedPartsDueToMissiles = scoreData.Sum(kvp => kvp.Value.totalDamagedPartsDueToMissiles); + BDACompetitionMode.Instance.Scores.RemovePlayer(vesselName); + BDACompetitionMode.Instance.Scores.AddPlayer(vessel); + BDACompetitionMode.Instance.Scores.ScoreData[vesselName].lastDamageTime = scoreData[spawnCount].lastDamageTime; + BDACompetitionMode.Instance.Scores.ScoreData[vesselName].lastPersonWhoDamagedMe = scoreData[spawnCount].lastPersonWhoDamagedMe; + } + } + } + + public void DumpContinuousSpawningScores(string tag = "") + { + var logStrings = new List(); + + if (continuousSpawningScores == null || continuousSpawningScores.Count == 0) return; + foreach (var vesselName in continuousSpawningScores.Keys) + UpdateCompetitionScores(continuousSpawningScores[vesselName].vessel); + BDACompetitionMode.Instance.competitionStatus.Add("Dumping scores for competition " + BDACompetitionMode.Instance.CompetitionID.ToString() + (tag != "" ? " " + tag : "")); + logStrings.Add("[BDArmory.VesselSpawner:" + BDACompetitionMode.Instance.CompetitionID + "]: Dumping Results at " + (int)(Planetarium.GetUniversalTime() - BDACompetitionMode.Instance.competitionStartTime) + "s"); + foreach (var vesselName in continuousSpawningScores.Keys) + { + var vesselScore = continuousSpawningScores[vesselName]; + var scoreData = vesselScore.scoreData; + logStrings.Add("[BDArmory.VesselSpawner:" + BDACompetitionMode.Instance.CompetitionID + "]: Name:" + vesselName); + logStrings.Add("[BDArmory.VesselSpawner:" + BDACompetitionMode.Instance.CompetitionID + "]: DEATHCOUNT:" + scoreData.Values.Where(v => v.deathTime >= 0).Count()); + var deathTimes = string.Join(";", scoreData.Values.Where(v => v.deathTime >= 0).Select(v => v.deathTime.ToString("0.0"))); + if (deathTimes != "") logStrings.Add("[BDArmory.VesselSpawner:" + BDACompetitionMode.Instance.CompetitionID + "]: DEATHTIMES:" + deathTimes); + #region Bullets + var whoShotMeScores = string.Join(", ", scoreData.Where(kvp => kvp.Value.hitCounts.Count > 0).Select(kvp => kvp.Key + ":" + string.Join(";", kvp.Value.hitCounts.Select(kvp2 => kvp2.Value + ":" + kvp2.Key)))); + if (whoShotMeScores != "") logStrings.Add("[BDArmory.VesselSpawner:" + BDACompetitionMode.Instance.CompetitionID + "]: WHOSHOTME:" + whoShotMeScores); + var whoDamagedMeWithBulletsScores = string.Join(", ", scoreData.Where(kvp => kvp.Value.damageFromGuns.Count > 0).Select(kvp => kvp.Key + ":" + string.Join(";", kvp.Value.damageFromGuns.Select(kvp2 => kvp2.Value.ToString("0.0") + ":" + kvp2.Key)))); + if (whoDamagedMeWithBulletsScores != "") logStrings.Add("[BDArmory.VesselSpawner:" + BDACompetitionMode.Instance.CompetitionID + "]: WHODAMAGEDMEWITHBULLETS:" + whoDamagedMeWithBulletsScores); + #endregion + #region Rockets + var whoStruckMeWithRocketsScores = string.Join(", ", scoreData.Where(kvp => kvp.Value.rocketStrikeCounts.Count > 0).Select(kvp => kvp.Key + ":" + string.Join(";", kvp.Value.rocketStrikeCounts.Select(kvp2 => kvp2.Value + ":" + kvp2.Key)))); + if (whoStruckMeWithRocketsScores != "") logStrings.Add("[BDArmory.VesselSpawner:" + BDACompetitionMode.Instance.CompetitionID + "]: WHOSTRUCKMEWITHROCKETS:" + whoStruckMeWithRocketsScores); + var whoPartsHitMeWithRocketsScores = string.Join(", ", scoreData.Where(kvp => kvp.Value.rocketPartDamageCounts.Count > 0).Select(kvp => kvp.Key + ":" + string.Join(";", kvp.Value.rocketPartDamageCounts.Select(kvp2 => kvp2.Value + ":" + kvp2.Key)))); + if (whoPartsHitMeWithRocketsScores != "") logStrings.Add("[BDArmory.VesselSpawner:" + BDACompetitionMode.Instance.CompetitionID + "]: WHOPARTSHITMEWITHROCKETS:" + whoPartsHitMeWithRocketsScores); + var whoDamagedMeWithRocketsScores = string.Join(", ", scoreData.Where(kvp => kvp.Value.damageFromRockets.Count > 0).Select(kvp => kvp.Key + ":" + string.Join(";", kvp.Value.damageFromRockets.Select(kvp2 => kvp2.Value.ToString("0.0") + ":" + kvp2.Key)))); + if (whoDamagedMeWithRocketsScores != "") logStrings.Add("[BDArmory.VesselSpawner:" + BDACompetitionMode.Instance.CompetitionID + "]: WHODAMAGEDMEWITHROCKETS:" + whoDamagedMeWithRocketsScores); + #endregion + #region Missiles + var whoStruckMeWithMissilesScores = string.Join(", ", scoreData.Where(kvp => kvp.Value.missileHitCounts.Count > 0).Select(kvp => kvp.Key + ":" + string.Join(";", kvp.Value.missileHitCounts.Select(kvp2 => kvp2.Value + ":" + kvp2.Key)))); + if (whoStruckMeWithMissilesScores != "") logStrings.Add("[BDArmory.VesselSpawner:" + BDACompetitionMode.Instance.CompetitionID + "]: WHOSTRUCKMEWITHMISSILES:" + whoStruckMeWithMissilesScores); + var whoPartsHitMeWithMissilesScores = string.Join(", ", scoreData.Where(kvp => kvp.Value.missilePartDamageCounts.Count > 0).Select(kvp => kvp.Key + ":" + string.Join(";", kvp.Value.missilePartDamageCounts.Select(kvp2 => kvp2.Value + ":" + kvp2.Key)))); + if (whoPartsHitMeWithMissilesScores != "") logStrings.Add("[BDArmory.VesselSpawner:" + BDACompetitionMode.Instance.CompetitionID + "]: WHOPARTSHITMEWITHMISSILES:" + whoPartsHitMeWithMissilesScores); + var whoDamagedMeWithMissilesScores = string.Join(", ", scoreData.Where(kvp => kvp.Value.damageFromMissiles.Count > 0).Select(kvp => kvp.Key + ":" + string.Join(";", kvp.Value.damageFromMissiles.Select(kvp2 => kvp2.Value.ToString("0.0") + ":" + kvp2.Key)))); + if (whoDamagedMeWithMissilesScores != "") logStrings.Add("[BDArmory.VesselSpawner:" + BDACompetitionMode.Instance.CompetitionID + "]: WHODAMAGEDMEWITHMISSILES:" + whoDamagedMeWithMissilesScores); + #endregion + #region Rams + var whoRammedMeScores = string.Join(", ", scoreData.Where(kvp => kvp.Value.rammingPartLossCounts.Count > 0).Select(kvp => kvp.Key + ":" + string.Join(";", kvp.Value.rammingPartLossCounts.Select(kvp2 => kvp2.Value + ":" + kvp2.Key)))); + if (whoRammedMeScores != "") logStrings.Add("[BDArmory.VesselSpawner:" + BDACompetitionMode.Instance.CompetitionID + "]: WHORAMMEDME:" + whoRammedMeScores); + #endregion + #region Kills + var GMKills = string.Join(", ", scoreData.Where(kvp => kvp.Value.gmKillReason != GMKillReason.None).Select(kvp => kvp.Key + ":" + kvp.Value.gmKillReason)); + if (GMKills != "") logStrings.Add("[BDArmory.VesselSpawner:" + BDACompetitionMode.Instance.CompetitionID + "]: GMKILL:" + GMKills); + var specialKills = new HashSet { AliveState.CleanKill, AliveState.HeadShot, AliveState.KillSteal }; // FIXME expand these to the separate special kill types + var cleanKills = string.Join(", ", scoreData.Where(kvp => specialKills.Contains(kvp.Value.aliveState) && kvp.Value.lastDamageWasFrom == DamageFrom.Guns).Select(kvp => kvp.Key + ":" + kvp.Value.lastPersonWhoDamagedMe)); + if (cleanKills != "") logStrings.Add("[BDArmory.VesselSpawner:" + BDACompetitionMode.Instance.CompetitionID + "]: CLEANKILL:" + cleanKills); + var cleanFrags = string.Join(", ", scoreData.Where(kvp => specialKills.Contains(kvp.Value.aliveState) && kvp.Value.lastDamageWasFrom == DamageFrom.Rockets).Select(kvp => kvp.Key + ":" + kvp.Value.lastPersonWhoDamagedMe)); + if (cleanFrags != "") logStrings.Add("[BDArmory.VesselSpawner:" + BDACompetitionMode.Instance.CompetitionID + "]: CLEANFRAG:" + cleanFrags); + var cleanRams = string.Join(", ", scoreData.Where(kvp => specialKills.Contains(kvp.Value.aliveState) && kvp.Value.lastDamageWasFrom == DamageFrom.Ramming).Select(kvp => kvp.Key + ":" + kvp.Value.lastPersonWhoDamagedMe)); + if (cleanRams != "") logStrings.Add("[BDArmory.VesselSpawner:" + BDACompetitionMode.Instance.CompetitionID + "]: CLEANRAM:" + cleanRams); + var cleanMissileKills = string.Join(", ", scoreData.Where(kvp => specialKills.Contains(kvp.Value.aliveState) && kvp.Value.lastDamageWasFrom == DamageFrom.Missiles).Select(kvp => kvp.Key + ":" + kvp.Value.lastPersonWhoDamagedMe)); + if (cleanMissileKills != "") logStrings.Add("[BDArmory.VesselSpawner:" + BDACompetitionMode.Instance.CompetitionID + "]: CLEANMISSILEKILL:" + cleanMissileKills); + #endregion + var accuracy = string.Join(", ", scoreData.Select(kvp => kvp.Key + ":" + kvp.Value.hits + "/" + kvp.Value.shotsFired + ":" + kvp.Value.rocketStrikes + "/" + kvp.Value.rocketsFired)); + if (accuracy != "") logStrings.Add("[BDArmory.VesselSpawner:" + BDACompetitionMode.Instance.CompetitionID + "]: ACCURACY:" + accuracy); + if (BDArmorySettings.TAG_MODE) + { + if (scoreData.Sum(kvp => kvp.Value.tagScore) > 0) logStrings.Add("[BDArmory.VesselSpawner:" + BDACompetitionMode.Instance.CompetitionID + "]: TAGSCORE:" + string.Join(", ", scoreData.Where(kvp => kvp.Value.tagScore > 0).Select(kvp => kvp.Key + ":" + kvp.Value.tagScore.ToString("0.0")))); + if (scoreData.Sum(kvp => kvp.Value.tagTotalTime) > 0) logStrings.Add("[BDArmory.VesselSpawner:" + BDACompetitionMode.Instance.CompetitionID + "]: TIMEIT:" + string.Join(", ", scoreData.Where(kvp => kvp.Value.tagTotalTime > 0).Select(kvp => kvp.Key + ":" + kvp.Value.tagTotalTime.ToString("0.0")))); + if (scoreData.Sum(kvp => kvp.Value.tagKillsWhileIt) > 0) logStrings.Add("[BDArmory.VesselSpawner:" + BDACompetitionMode.Instance.CompetitionID + "]: KILLSWHILEIT:" + string.Join(", ", scoreData.Where(kvp => kvp.Value.tagKillsWhileIt > 0).Select(kvp => kvp.Key + ":" + kvp.Value.tagKillsWhileIt))); + if (scoreData.Sum(kvp => kvp.Value.tagTimesIt) > 0) logStrings.Add("[BDArmory.VesselSpawner:" + BDACompetitionMode.Instance.CompetitionID + "]: TIMESIT:" + string.Join(", ", scoreData.Where(kvp => kvp.Value.tagTimesIt > 0).Select(kvp => kvp.Key + ":" + kvp.Value.tagTimesIt))); + } + } + + // Dump the log results to a file. + if (BDACompetitionMode.Instance.CompetitionID > 0) + { + var folder = Path.Combine(KSPUtil.ApplicationRootPath, "GameData", "BDArmory", "Logs"); + if (!Directory.Exists(folder)) + Directory.CreateDirectory(folder); + File.WriteAllLines(Path.Combine(folder, "cts-" + BDACompetitionMode.Instance.CompetitionID.ToString() + (tag != "" ? "-" + tag : "") + ".log"), logStrings); + } + } + #endregion + + public void KillOffOutOfAmmoVessels() + { + if (BDArmorySettings.OUT_OF_AMMO_KILL_TIME < 0) return; // Never + var now = Planetarium.GetUniversalTime(); + Vessel vessel; + MissileFire weaponManager; + ContinuousSpawningScores score; + foreach (var vesselName in continuousSpawningScores.Keys) + { + score = continuousSpawningScores[vesselName]; + vessel = score.vessel; + if (vessel == null) continue; // Vessel hasn't been respawned yet. + weaponManager = VesselModuleRegistry.GetModule(vessel); + if (weaponManager == null) continue; // Weapon manager hasn't registered yet. + if (score.outOfAmmoTime == 0 && !weaponManager.HasWeaponsAndAmmo()) + score.outOfAmmoTime = Planetarium.GetUniversalTime(); + if (score.outOfAmmoTime > 0 && now - score.outOfAmmoTime > BDArmorySettings.OUT_OF_AMMO_KILL_TIME) + { + LogMessage("Killing off " + vesselName + " as they exceeded the out-of-ammo kill time."); + BDACompetitionMode.Instance.Scores.RegisterDeath(vesselName, GMKillReason.OutOfAmmo); // Indicate that it was us who killed it and remove any "clean" kills. + SpawnUtils.RemoveVessel(vessel); + } + } + } + } +} \ No newline at end of file diff --git a/BDArmory/VesselSpawning/CustomSpawnTemplate.cs b/BDArmory/VesselSpawning/CustomSpawnTemplate.cs new file mode 100644 index 000000000..decb98685 --- /dev/null +++ b/BDArmory/VesselSpawning/CustomSpawnTemplate.cs @@ -0,0 +1,988 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using UnityEngine; +using KSP.UI.Screens; + +using BDArmory.Competition; +using BDArmory.Extensions; +using BDArmory.Settings; +using BDArmory.UI; +using BDArmory.Utils; + +namespace BDArmory.VesselSpawning +{ + /// + /// Spawn teams of craft in a custom template. + /// + [KSPAddon(KSPAddon.Startup.Flight, false)] + public class CustomTemplateSpawning : VesselSpawnerBase + { + public static CustomTemplateSpawning Instance; + void LogMessage(string message, bool toScreen = true, bool toLog = true) => LogMessageFrom("CustomTemplateSpawning", message, toScreen, toLog); + + [CustomSpawnTemplateField] public static List customSpawnConfigs = null; + bool startCompetitionAfterSpawning = false; + protected override void Awake() + { + base.Awake(); + if (Instance != null) Destroy(Instance); + Instance = this; + + if (customSpawnConfigs == null) customSpawnConfigs = new List(); + LoadTemplate(null, true); + } + + void Start() + { + StartCoroutine(WaitForBDASettings()); + } + + IEnumerator WaitForBDASettings() + { + yield return new WaitUntil(() => BDArmorySetup.Instance is not null); + if (_crewGUICheckIndex < 0) _crewGUICheckIndex = GUIUtils.RegisterGUIRect(new Rect()); + if (_vesselGUICheckIndex < 0) _vesselGUICheckIndex = GUIUtils.RegisterGUIRect(new Rect()); + } + + void OnDestroy() + { + CustomSpawnTemplateField.Save(); + } + + public override IEnumerator Spawn(SpawnConfig spawnConfig) + { + var customSpawnConfig = spawnConfig as CustomSpawnConfig; + if (customSpawnConfig == null) yield break; + SpawnCustomTemplateAsCoroutine(customSpawnConfig); + } + + public void CancelSpawning() + { + if (vesselsSpawning) + { + vesselsSpawning = false; + LogMessage("Vessel spawning cancelled."); + } + if (spawnCustomTemplateCoroutine != null) + { + StopCoroutine(spawnCustomTemplateCoroutine); + spawnCustomTemplateCoroutine = null; + } + } + + #region Custom template spawning + /// + /// Prespawn initialisation to handle camera and body changes and to ensure that only a single spawning coroutine is running. + /// + /// The spawn config for the new spawning. + public override void PreSpawnInitialisation(SpawnConfig spawnConfig) + { + if (craftBrowser != null) craftBrowser = null; // Clean up the craft browser. + HideOtherWindows(null); // Make sure the template/vessel/crew selection windows are hidden. + + base.PreSpawnInitialisation(spawnConfig); + + vesselsSpawning = true; // Signal that we've started the spawning vessels routine. + vesselSpawnSuccess = false; // Set our success flag to false for now. + spawnFailureReason = SpawnFailureReason.None; // Reset the spawn failure reason. + if (spawnCustomTemplateCoroutine != null) + StopCoroutine(spawnCustomTemplateCoroutine); + } + + public void SpawnCustomTemplate(CustomSpawnConfig spawnConfig) + { + if (spawnConfig == null) return; + PreSpawnInitialisation(spawnConfig); + spawnCustomTemplateCoroutine = StartCoroutine(SpawnCustomTemplateCoroutine(spawnConfig)); + LogMessage("Triggering vessel spawning at " + spawnConfig.latitude.ToString("G6") + ", " + spawnConfig.longitude.ToString("G6") + ", with altitude " + spawnConfig.altitude + "m.", false); + } + + /// + /// A coroutine version of the SpawnCustomTemplate function that performs the required prespawn initialisation. + /// + /// The spawn config to use. + public IEnumerator SpawnCustomTemplateAsCoroutine(CustomSpawnConfig spawnConfig) + { + PreSpawnInitialisation(spawnConfig); + LogMessage("Triggering vessel spawning at " + spawnConfig.latitude.ToString("G6") + ", " + spawnConfig.longitude.ToString("G6") + ", with altitude " + spawnConfig.altitude + "m.", false); + yield return SpawnCustomTemplateCoroutine(spawnConfig); + } + + private Coroutine spawnCustomTemplateCoroutine; + // Spawns all vessels in an outward facing ring and lowers them to the ground. An altitude of 5m should be suitable for most cases. + private IEnumerator SpawnCustomTemplateCoroutine(CustomSpawnConfig spawnConfig) + { + #region Initialisation and sanity checks + // Tally up the craft to spawn and figure out teams. + spawnConfig.craftFiles = spawnConfig.customVesselSpawnConfigs.SelectMany(team => team).Select(config => config.craftURL).Where(craftURL => !string.IsNullOrEmpty(craftURL)).ToList(); + LogMessage("Spawning " + spawnConfig.craftFiles.Count + " vessels at an altitude of " + spawnConfig.altitude.ToString("G0") + "m" + (spawnConfig.craftFiles.Count > 8 ? ", this may take some time..." : ".")); + #endregion + + yield return AcquireSpawnPoint(spawnConfig, 100f, false); + if (spawnFailureReason != SpawnFailureReason.None) + { + vesselsSpawning = false; + SpawnUtils.RevertSpawnLocationCamera(true, true); + yield break; + } + + // Configure the vessels' individual spawn configs. + var vesselSpawnConfigs = new List(); + foreach (var customVesselSpawnConfig in spawnConfig.customVesselSpawnConfigs.SelectMany(config => config)) + { + if (string.IsNullOrEmpty(customVesselSpawnConfig.craftURL)) continue; + + var vesselSpawnPoint = FlightGlobals.currentMainBody.GetWorldSurfacePosition(customVesselSpawnConfig.latitude, customVesselSpawnConfig.longitude, spawnConfig.altitude); + var radialUnitVector = (vesselSpawnPoint - FlightGlobals.currentMainBody.transform.position).normalized; + var refDirection = Math.Abs(Vector3.Dot(Vector3.up, radialUnitVector)) < 0.71f ? Vector3.up : Vector3.forward; // Avoid that the reference direction is colinear with the local surface normal. + var crew = new List(); + if (!string.IsNullOrEmpty(customVesselSpawnConfig.kerbalName)) crew.Add(HighLogic.CurrentGame.CrewRoster[customVesselSpawnConfig.kerbalName]); + vesselSpawnConfigs.Add(new VesselSpawnConfig( + customVesselSpawnConfig.craftURL, + vesselSpawnPoint, + (Quaternion.AngleAxis(customVesselSpawnConfig.heading, radialUnitVector) * refDirection).ProjectOnPlanePreNormalized(radialUnitVector).normalized, + (float)spawnConfig.altitude, + 0, + false, + customVesselSpawnConfig.teamIndex, + false, + crew + )); + } + VesselSpawner.ReservedCrew = vesselSpawnConfigs.Where(config => config.crew.Count > 0).SelectMany(config => config.crew).Select(crew => crew.name).ToHashSet(); + foreach (var crew in vesselSpawnConfigs.Where(config => config.crew.Count > 0).SelectMany(config => config.crew)) crew.rosterStatus = ProtoCrewMember.RosterStatus.Available; // Set all the requested crew as available since we've just killed off everything. + + yield return SpawnVessels(vesselSpawnConfigs); + VesselSpawner.ReservedCrew.Clear(); + if (spawnFailureReason != SpawnFailureReason.None) + { + vesselsSpawning = false; + SpawnUtils.RevertSpawnLocationCamera(true, true); + yield break; + } + + #region Post-spawning + // Revert back to the KSP's proper camera. + SpawnUtils.RevertSpawnLocationCamera(true); + + // Spawning has succeeded, vessels have been renamed where necessary and vessels are ready. Time to assign teams and any other stuff. + yield return PostSpawnMainSequence(spawnConfig, false, !startCompetitionAfterSpawning); + if (spawnFailureReason != SpawnFailureReason.None) + { + LogMessage("Vessel spawning FAILED! " + spawnFailureReason); + vesselsSpawning = false; + SpawnUtils.RevertSpawnLocationCamera(true, true); + yield break; + } + + // Revert the camera and focus on one of the vessels. + if ((FlightGlobals.ActiveVessel == null || FlightGlobals.ActiveVessel.state == Vessel.State.DEAD) && spawnedVessels.Count > 0) + { + yield return LoadedVesselSwitcher.Instance.SwitchToVesselWhenPossible(spawnedVessels.Take(UnityEngine.Random.Range(1, spawnedVessels.Count)).Last().Value); // Update the camera. + } + FlightCamera.fetch.SetDistance(50); + + // Assign the vessels to teams. + LogMessage("Assigning vessels to teams.", false); + var teamVesselNames = new List>(); + for (int i = 0; i < spawnedVesselsTeamIndex.Max(kvp => kvp.Value); ++i) + teamVesselNames.Add(spawnedVesselsTeamIndex.Where(kvp => kvp.Value == i).Select(kvp => kvp.Key).ToList()); + LoadedVesselSwitcher.Instance.MassTeamSwitch(true, false, null, teamVesselNames); // Assign A, B, ... + #endregion + + LogMessage("Vessel spawning SUCCEEDED!", true, BDArmorySettings.DEBUG_SPAWNING); + vesselSpawnSuccess = true; + vesselsSpawning = false; + + if (startCompetitionAfterSpawning) + { + // Run the competition. + BDACompetitionMode.Instance.StartCompetitionMode(BDArmorySettings.COMPETITION_DISTANCE, BDArmorySettings.COMPETITION_START_DESPITE_FAILURES); + } + } + #endregion + + #region Templates + public CustomSpawnConfig customSpawnConfig = null; + /// + /// Reload all the templates from disk and return the specified one or an empty one if no name (or an invalid one) was specified. + /// + /// The name of the template to load. + public void LoadTemplate(string templateName = null, bool fromDisk = false) + { + if (fromDisk) // Reload the templates from disk. + CustomSpawnTemplateField.Load(); + else if (templateName != null && templateName == customSpawnConfig.name) + { + RefreshSelectedCrew(); + return; // It's the same config, which hasn't been adjusted, so return it without clearing the fields. + } + + // Find a matching config. + if (templateName != null) customSpawnConfig = customSpawnConfigs.Find(config => config.name == templateName); + // Otherwise, return an empty one. + if (customSpawnConfig == null) + { + customSpawnConfig = new CustomSpawnConfig( + "", + new SpawnConfig( + worldIndex: BDArmorySettings.VESSEL_SPAWN_WORLDINDEX, + latitude: BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.x, + longitude: BDArmorySettings.VESSEL_SPAWN_GEOCOORDS.y, + altitude: BDArmorySettings.VESSEL_SPAWN_ALTITUDE + ), + new List>() + ); + } + RefreshSelectedCrew(); + } + + /// + /// Update the current template with new spawn points from the LoadedVesselSwitcher. + /// + public void SaveTemplate() + { + if (LoadedVesselSwitcher.Instance.WeaponManagers.Count == 0) return; // Safe-guard, don't save over an existing template when the slots are empty. + var geoCoords = FlightGlobals.currentMainBody.GetLatitudeAndLongitude(LoadedVesselSwitcher.Instance.WeaponManagers.SelectMany(tm => tm.Value).Select(wm => wm.vessel.transform.position).Aggregate(Vector3.zero, (l, r) => l + r) / LoadedVesselSwitcher.Instance.WeaponManagers.SelectMany(tm => tm.Value).Count()); // Set the central spawn location at the centroid of the craft. + customSpawnConfig.worldIndex = BDArmorySettings.VESSEL_SPAWN_WORLDINDEX; + customSpawnConfig.latitude = geoCoords.x; + customSpawnConfig.longitude = geoCoords.y; + customSpawnConfig.altitude = BDArmorySettings.VESSEL_SPAWN_ALTITUDE; + customSpawnConfig.customVesselSpawnConfigs.Clear(); + int teamCount = 0; + foreach (var team in LoadedVesselSwitcher.Instance.WeaponManagers) + { + var teamConfigs = new List(); + foreach (var member in team.Value) + { + geoCoords = FlightGlobals.currentMainBody.GetLatitudeAndLongitude(member.vessel.transform.position); + CustomVesselSpawnConfig vesselSpawnConfig = new CustomVesselSpawnConfig( + geoCoords.x, + geoCoords.y, + (Vector3.SignedAngle(member.vessel.north, member.vessel.ReferenceTransform.up, member.vessel.up) + 360f) % 360f, + teamCount + ); + teamConfigs.Add(vesselSpawnConfig); + } + customSpawnConfig.customVesselSpawnConfigs.Add(teamConfigs); + ++teamCount; + } + if (!customSpawnConfigs.Contains(customSpawnConfig)) customSpawnConfigs.Add(customSpawnConfig); // Add the template if it isn't already there. + CustomSpawnTemplateField.Save(); + PopulateEntriesFromLVS(); // Populate the slots to show the layout. + } + + /// + /// Create a template from the current vessels in the Vessel Switcher. + /// Vessel positions, rotations and teams are saved. + /// + /// + public CustomSpawnConfig NewTemplate(string templateName="") + { + // Remove any invalid or unnamed entries. + customSpawnConfigs = customSpawnConfigs.Where(config => !string.IsNullOrEmpty(config.name) && config.customVesselSpawnConfigs.Count > 0).ToList(); + + // Then make a new one. + var geoCoords = FlightGlobals.currentMainBody.GetLatitudeAndLongitude(LoadedVesselSwitcher.Instance.WeaponManagers.SelectMany(tm => tm.Value).Select(wm => wm.vessel.transform.position).Aggregate(Vector3.zero, (l, r) => l + r) / LoadedVesselSwitcher.Instance.WeaponManagers.SelectMany(tm => tm.Value).Count()); // Set the central spawn location at the centroid of the craft. + customSpawnConfig = new CustomSpawnConfig( + templateName, + new SpawnConfig( + worldIndex: BDArmorySettings.VESSEL_SPAWN_WORLDINDEX, + latitude: geoCoords.x, + longitude: geoCoords.y, + altitude: BDArmorySettings.VESSEL_SPAWN_ALTITUDE + ), + new List>() + ); + int teamCount = 0; + foreach (var team in LoadedVesselSwitcher.Instance.WeaponManagers) + { + var teamConfigs = new List(); + foreach (var member in team.Value) + { + geoCoords = FlightGlobals.currentMainBody.GetLatitudeAndLongitude(member.vessel.transform.position); + CustomVesselSpawnConfig vesselSpawnConfig = new CustomVesselSpawnConfig( + geoCoords.x, + geoCoords.y, + (Vector3.SignedAngle(member.vessel.north, member.vessel.ReferenceTransform.up, member.vessel.up) + 360f) % 360f, + teamCount + ); + teamConfigs.Add(vesselSpawnConfig); + } + customSpawnConfig.customVesselSpawnConfigs.Add(teamConfigs); + ++teamCount; + } + customSpawnConfigs.Add(customSpawnConfig); + CustomSpawnTemplateField.Save(); + PopulateEntriesFromLVS(); // Populate the slots to show the layout. + return customSpawnConfig; + } + + /// + /// Populate the spawn slots from the current vessels and kerbals in the loaded vessel switcher. + /// + void PopulateEntriesFromLVS() + { + Func distanceToRoot = null; + distanceToRoot = (p) => { return p.parent != null ? distanceToRoot(p.parent) : 0; }; + if (CustomCraftBrowserDialog.shipNames.Count == 0) + { + craftBrowser = new CustomCraftBrowserDialog(); + craftBrowser.UpdateList(craftBrowser.facility); + } + SelectedCrewMembers.Clear(); + + using (var team = LoadedVesselSwitcher.Instance.WeaponManagers.GetEnumerator()) + using (var teamSlot = customSpawnConfig.customVesselSpawnConfigs.GetEnumerator()) + while (team.MoveNext() && teamSlot.MoveNext()) + { + using (var member = team.Current.Value.GetEnumerator()) + using (var memberSlot = teamSlot.Current.GetEnumerator()) + while (member.MoveNext() && memberSlot.MoveNext()) + { + // Find the craft with the matching name. + memberSlot.Current.craftURL = CustomCraftBrowserDialog.shipNames.FirstOrDefault(c => c.Value == member.Current.vessel.vesselName).Key; + if (string.IsNullOrEmpty(memberSlot.Current.craftURL)) + { + // Try stripping _1, etc. from the end of the vesselName + var lastIndex = member.Current.vessel.vesselName.LastIndexOf("_"); + if (lastIndex > 0) + { + var possibleName = member.Current.vessel.vesselName.Substring(0, lastIndex); + memberSlot.Current.craftURL = CustomCraftBrowserDialog.shipNames.FirstOrDefault(c => c.Value == possibleName).Key; + } + } + // Find the primary crew onboard. + var crewParts = member.Current.vessel.parts.FindAll(p => p.protoModuleCrew.Count > 0).OrderBy(p => distanceToRoot(p)).ToList(); + if (crewParts.Count > 0) + { + memberSlot.Current.kerbalName = crewParts.First().protoModuleCrew.First().name; + SelectedCrewMembers.Add(memberSlot.Current.kerbalName); + } + else + { + memberSlot.Current.kerbalName = null; + } + } + } + } + + /// + /// Configure the spawn template with locally settable config values and perform a sanity check for being able to run a competition. + /// + /// true if there are sufficient non-empty teams for a competition, false otherwise + public bool ConfigureTemplate(bool startCompetitionAfterSpawning) + { + // Sanity check + if (startCompetitionAfterSpawning && customSpawnConfig.customVesselSpawnConfigs.Count(team => team.Count(cfg => !string.IsNullOrEmpty(cfg.craftURL)) > 0) < 2) // At least two non-empty teams. + { + BDACompetitionMode.Instance.competitionStatus.Add("Not enough vessels selected for a competition."); + return false; + } + + // Set the locally settable config values. + customSpawnConfig.altitude = Mathf.Clamp(BDArmorySettings.VESSEL_SPAWN_ALTITUDE, 2f, 10f); + customSpawnConfig.killEverythingFirst = true; + + this.startCompetitionAfterSpawning = startCompetitionAfterSpawning; + return true; + } + + #endregion + + #region UI + void OnGUI() + { + if (!HighLogic.LoadedSceneIsFlight) return; + if (!BDArmorySetup.GAME_UI_ENABLED) return; + if (showTemplateSelection) + { + if (Event.current.type == EventType.MouseDown && !templateSelectionWindowRect.Contains(Event.current.mousePosition)) + HideTemplateSelection(); + else + templateSelectionWindowRect = GUILayout.Window(GUIUtility.GetControlID(FocusType.Passive), templateSelectionWindowRect, TemplateSelectionWindow, StringUtils.Localize("#LOC_BDArmory_Settings_CustomSpawnTemplate_TemplateSelection"), BDArmorySetup.BDGuiSkin.window); + } + if (showCrewSelection) + { + if (Event.current.type == EventType.MouseDown && !crewSelectionWindowRect.Contains(Event.current.mousePosition)) + HideCrewSelection(); + else + crewSelectionWindowRect = GUILayout.Window(GUIUtility.GetControlID(FocusType.Passive), crewSelectionWindowRect, CrewSelectionWindow, StringUtils.Localize("#LOC_BDArmory_Settings_CustomSpawnTemplate_CrewSelection"), BDArmorySetup.BDGuiSkin.window); + } + if (showVesselSelection) + { + if (Event.current.type == EventType.MouseDown && !vesselSelectionWindowRect.Contains(Event.current.mousePosition)) + HideVesselSelection(); + else + vesselSelectionWindowRect = GUILayout.Window(GUIUtility.GetControlID(FocusType.Passive), vesselSelectionWindowRect, VesselSelectionWindow, StringUtils.Localize("#LOC_BDArmory_Settings_CustomSpawnTemplate_VesselSelection"), BDArmorySetup.BDGuiSkin.window); + } + } + + #region Template Selection + internal static int _templateGUICheckIndex = -1; + bool showTemplateSelection = false; + bool bringTemplateSelectionToFront = false; + Rect templateSelectionWindowRect = new Rect(0, 0, 300, 200); + Vector2 templateSelectionScrollPos = default; + /// + /// Show the template section window. + /// + /// The mouse click position. + public void ShowTemplateSelection(Vector2 position) + { + HideOtherWindows("template"); + templateSelectionWindowRect.position = position + new Vector2(-templateSelectionWindowRect.width / 2, 20); // Centred and slightly below. + showTemplateSelection = true; + bringTemplateSelectionToFront = true; + GUIUtils.SetGUIRectVisible(_templateGUICheckIndex, true); + } + + /// + /// Hide the template selection window. + /// + void HideTemplateSelection() + { + showTemplateSelection = false; + GUIUtils.SetGUIRectVisible(_templateGUICheckIndex, false); + } + + CustomSpawnConfig templateToRemove = null; + public void TemplateSelectionWindow(int windowID) + { + GUI.DragWindow(new Rect(0, 0, templateSelectionWindowRect.width, 20)); + GUILayout.BeginVertical(); + templateSelectionScrollPos = GUILayout.BeginScrollView(templateSelectionScrollPos, GUI.skin.box, GUILayout.Width(templateSelectionWindowRect.width - 15), GUILayout.MaxHeight(templateSelectionWindowRect.height - 10)); + using (var templateName = customSpawnConfigs.GetEnumerator()) + while (templateName.MoveNext()) + { + if (string.IsNullOrEmpty(templateName.Current.name) || templateName.Current.customVesselSpawnConfigs.Count == 0) continue; // Skip any empty or unnamed templates. + GUILayout.BeginHorizontal(); + if (GUILayout.Button(templateName.Current.name, BDArmorySetup.BDGuiSkin.button)) + { + LoadTemplate(templateName.Current.name, Event.current.button == 1); // Right click to reload templates from disk. + HideTemplateSelection(); + } + if (GUILayout.Button(" X", BDArmorySetup.CloseButtonStyle, GUILayout.Width(24))) + { + templateToRemove = templateName.Current; + } + GUILayout.EndHorizontal(); + } + if (templateToRemove != null) + { + customSpawnConfigs.Remove(templateToRemove); + if (templateToRemove == customSpawnConfig) customSpawnConfig = NewTemplate(); + templateToRemove = null; + } + GUILayout.EndScrollView(); + GUILayout.EndVertical(); + GUIUtils.RepositionWindow(ref templateSelectionWindowRect); + GUIUtils.UpdateGUIRect(templateSelectionWindowRect, _templateGUICheckIndex); + GUIUtils.UseMouseEventInRect(templateSelectionWindowRect); + if (bringTemplateSelectionToFront) + { + GUI.BringWindowToFront(windowID); + bringTemplateSelectionToFront = false; + } + } + #endregion + + #region Vessel Selection + CustomVesselSpawnConfig currentVesselSpawnConfig; + List currentTeamSpawnConfigs; + internal static int _vesselGUICheckIndex = -1; + bool showVesselSelection = false; + bool bringVesselSelectionToFront = false; + Rect vesselSelectionWindowRect = new Rect(0, 0, 600, 800); + Vector2 vesselSelectionScrollPos = default; + string selectionFilter = ""; + bool focusFilterField = false; + + CustomCraftBrowserDialog craftBrowser; + public string ShipName(string craft) => (!string.IsNullOrEmpty(craft) && CustomCraftBrowserDialog.shipNames.TryGetValue(craft, out string shipName)) ? shipName : ""; + + /// + /// Show the vessel selection window. + /// + /// Position of the mouse click. + /// The URL of the craft. + public void ShowVesselSelection(Vector2 position, CustomVesselSpawnConfig vesselSpawnConfig, List teamSpawnConfigs) + { + HideOtherWindows("vessel"); + if (showVesselSelection && vesselSpawnConfig == currentVesselSpawnConfig) + { + HideVesselSelection(); + return; + } + currentVesselSpawnConfig = vesselSpawnConfig; + currentTeamSpawnConfigs = teamSpawnConfigs; + if (craftBrowser == null) + { + craftBrowser = new CustomCraftBrowserDialog(); + craftBrowser.UpdateList(craftBrowser.facility); + } + vesselSelectionWindowRect.position = position + new Vector2(-vesselSelectionWindowRect.width - 120, -vesselSelectionWindowRect.height / 2); // Centred and slightly offset to allow clicking the same spot. + showVesselSelection = true; + focusFilterField = true; // Focus the filter text field. + bringVesselSelectionToFront = true; + GUIUtils.SetGUIRectVisible(_vesselGUICheckIndex, true); + } + + /// + /// Hide the vessel selection window. + /// + public void HideVesselSelection(CustomVesselSpawnConfig vesselSpawnConfig = null) + { + if (vesselSpawnConfig != null) + { + vesselSpawnConfig.craftURL = null; + } + showVesselSelection = false; + GUIUtils.SetGUIRectVisible(_vesselGUICheckIndex, false); + } + + public void VesselSelectionWindow(int windowID) + { + GUI.DragWindow(new Rect(0, 0, vesselSelectionWindowRect.width, 20)); + GUILayout.BeginVertical(); + selectionFilter = GUIUtils.TextField(selectionFilter, " Filter", "CSTFilterField"); + if (focusFilterField) + { + GUI.FocusControl("CSTFilterField"); + focusFilterField = false; + } + vesselSelectionScrollPos = GUILayout.BeginScrollView(vesselSelectionScrollPos, GUI.skin.box, GUILayout.Width(vesselSelectionWindowRect.width - 15), GUILayout.MaxHeight(vesselSelectionWindowRect.height - 60)); + using (var vessels = craftBrowser.craftList.GetEnumerator()) + while (vessels.MoveNext()) + { + var vesselURL = vessels.Current.Key; + var vesselInfo = vessels.Current.Value; + if (vesselURL == null || vesselInfo == null) continue; + if (!string.IsNullOrEmpty(selectionFilter)) // Filter selection, case insensitive. + { + if (!vesselInfo.shipName.ToLower().Contains(selectionFilter.ToLower())) continue; + } + GUILayout.BeginHorizontal(); // Vessel buttons + if (GUILayout.Button($"{vesselInfo.shipName}", CustomCraftBrowserDialog.vesselButtonStyle, GUILayout.MaxHeight(60), GUILayout.MaxWidth(vesselSelectionWindowRect.width - 190))) + { + currentVesselSpawnConfig.craftURL = vesselURL; + foreach (var vesselSpawnConfig in currentTeamSpawnConfigs) // Set the other empty slots for the team to the same vessel. + { + if (BDArmorySettings.CUSTOM_SPAWN_TEMPLATE_REPLACE_TEAM || string.IsNullOrEmpty(vesselSpawnConfig.craftURL)) + { + vesselSpawnConfig.craftURL = vesselURL; + } + } + HideVesselSelection(); + } + GUILayout.Label($"{StringUtils.Localize("#LOC_BDArmory_Settings_CustomSpawnTemplate_Parts")}: {vesselInfo.partCount}, {StringUtils.Localize("#LOC_BDArmory_Settings_CustomSpawnTemplate_Mass")}: {(vesselInfo.totalMass < 1000f ? $"{vesselInfo.totalMass:G3}t" : $"{vesselInfo.totalMass / 1000f:G3}kt")}\n{(vesselInfo.UnavailableShipParts.Count > 0 ? $"{StringUtils.Localize("#LOC_BDArmory_Settings_CustomSpawnTemplate_InvalidParts")}" : $"{StringUtils.Localize("#LOC_BDArmory_Settings_CustomSpawnTemplate_Version")}: {(vesselInfo.compatibility == VersionCompareResult.COMPATIBLE ? $"{vesselInfo.version}" : $"{vesselInfo.version}")}{(vesselInfo.UnavailableShipPartModules.Count > 0 ? $" {StringUtils.Localize("#LOC_BDArmory_Settings_CustomSpawnTemplate_UnknownModules")}" : "")}")}", CustomCraftBrowserDialog.vesselInfoStyle); + GUILayout.EndHorizontal(); + } + GUILayout.EndScrollView(); + GUILayout.Space(5); + GUILayout.BeginHorizontal(); // A line for various options + BDArmorySettings.CUSTOM_SPAWN_TEMPLATE_REPLACE_TEAM = GUILayout.Toggle(BDArmorySettings.CUSTOM_SPAWN_TEMPLATE_REPLACE_TEAM, StringUtils.Localize("#LOC_BDArmory_Settings_CustomSpawnTemplate_ReplaceTeam")); + GUILayout.EndHorizontal(); + GUILayout.Space(10); + GUILayout.BeginHorizontal(); + if (GUILayout.Button(StringUtils.Localize("#LOC_BDArmory_Settings_CustomSpawnTemplate_Clear"), BDArmorySetup.BDGuiSkin.button)) + { + currentVesselSpawnConfig.craftURL = null; + HideVesselSelection(); + } + if (GUILayout.Button(StringUtils.Localize("#LOC_BDArmory_Settings_CustomSpawnTemplate_ClearAll"), BDArmorySetup.BDGuiSkin.button)) + { + foreach (var team in customSpawnConfig.customVesselSpawnConfigs) + foreach (var member in team) + member.craftURL = null; + } + if (GUILayout.Button(StringUtils.Localize("#LOC_BDArmory_Settings_CustomSpawnTemplate_Refresh"), BDArmorySetup.BDGuiSkin.button)) + { craftBrowser.UpdateList(craftBrowser.facility); } + GUILayout.EndHorizontal(); + GUILayout.EndVertical(); + GUIUtils.RepositionWindow(ref vesselSelectionWindowRect); + GUIUtils.UpdateGUIRect(vesselSelectionWindowRect, _vesselGUICheckIndex); + GUIUtils.UseMouseEventInRect(vesselSelectionWindowRect); + if (bringVesselSelectionToFront) + { + bringVesselSelectionToFront = false; + GUI.BringWindowToFront(windowID); + } + } + + internal class CustomCraftBrowserDialog + { + public EditorFacility facility = EditorFacility.SPH; + string profile = HighLogic.SaveFolder; + string craftFolder; + public Dictionary craftList = new Dictionary(); + public Dictionary crewCounts = new Dictionary(); + public Action selectFileCallback = null; + public Action cancelledCallback = null; + public static Dictionary shipNames = new Dictionary(); + public static GUIStyle vesselButtonStyle = new GUIStyle(BDArmorySetup.BDGuiSkin.button); + public static GUIStyle vesselInfoStyle = new GUIStyle(BDArmorySetup.BDGuiSkin.label); + public void UpdateList(EditorFacility facility) + { + this.facility = facility; + craftFolder = Path.GetFullPath(Path.Combine(KSPUtil.ApplicationRootPath, "saves", profile, "Ships", facility.ToString())); + craftList = Directory.GetFiles(craftFolder, "*.craft").ToDictionary(craft => craft, craft => new CraftProfileInfo()); + if (craftList.ContainsKey(Path.Combine(craftFolder, "Auto-Saved Ship.craft"))) craftList.Remove(Path.Combine(craftFolder, "Auto-Saved Ship.craft")); // Ignore the Auto-Saved Ship. + CraftProfileInfo.PrepareCraftMetaFileLoad(); + foreach (var craft in craftList.Keys.ToList()) + { + var craftMeta = Path.Combine(craftFolder, $"{Path.GetFileNameWithoutExtension(craft)}.loadmeta"); + if (File.Exists(craftMeta)) // If the loadMeta file exists, use it, otherwise generate one. + { + craftList[craft].LoadFromMetaFile(craftMeta); + } + else + { + var craftNode = ConfigNode.Load(craft); + craftList[craft].LoadDetailsFromCraftFile(craftNode, craft); + craftList[craft].SaveToMetaFile(craftMeta); + } + } + crewCounts = craftList.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.partNames.Where(p => SpawnUtils.PartCrewCounts.ContainsKey(p)).Sum(p => SpawnUtils.PartCrewCounts[p])); + vesselButtonStyle.stretchHeight = true; + vesselButtonStyle.fontSize = 20; + vesselInfoStyle.fontSize = 12; + vesselInfoStyle.richText = true; + shipNames.Clear(); + foreach (var craft in craftList.Keys) + shipNames[craft] = craftList[craft].shipName; + } + } + + #endregion + + #region Crew Selection + internal static int _crewGUICheckIndex = -1; + bool showCrewSelection = false; + bool bringCrewSelectionToFront = false; + Rect crewSelectionWindowRect = new Rect(0, 0, 300, 400); + Vector2 crewSelectionScrollPos = default; + HashSet SelectedCrewMembers = new HashSet(); + HashSet ObserverCrewMembers = new HashSet(); + HashSet ActiveCrewMembers = new HashSet(); + public bool IsCrewSelectionShowing => showCrewSelection; + + /// + /// Show the crew selection window. + /// + /// Position of the mouse click. + /// The VesselSpawnConfig clicked on. + public void ShowCrewSelection(Vector2 position, CustomVesselSpawnConfig vesselSpawnConfig, bool ignoreActive = false) + { + HideOtherWindows("crew"); + if (showCrewSelection && vesselSpawnConfig == currentVesselSpawnConfig) + { + HideCrewSelection(); + return; + } + currentVesselSpawnConfig = vesselSpawnConfig; + crewSelectionWindowRect.position = position + new Vector2(50, -crewSelectionWindowRect.height / 2); // Centred and slightly offset to allow clicking the same spot. + showCrewSelection = true; + bringCrewSelectionToFront = true; + if (ignoreActive) + { + // Find any crew on active vessels. + foreach (var vessel in FlightGlobals.Vessels) + { + if (vessel == null || !vessel.loaded) continue; + foreach (var part in vessel.Parts) + { + if (part == null) continue; + foreach (var crew in part.protoModuleCrew) + { + if (crew == null) continue; + ActiveCrewMembers.Add(crew.name); + } + } + } + } + else { ActiveCrewMembers.Clear(); } + GUIUtils.SetGUIRectVisible(_crewGUICheckIndex, true); + foreach (var crew in HighLogic.CurrentGame.CrewRoster.Kerbals(ProtoCrewMember.KerbalType.Crew)) // Set any non-assigned crew as available. + { + if (crew.rosterStatus != ProtoCrewMember.RosterStatus.Assigned) + crew.rosterStatus = ProtoCrewMember.RosterStatus.Available; + } + RefreshObserverCrewMembers(); + } + + /// + /// Hide the crew selection window. + /// + public void HideCrewSelection(CustomVesselSpawnConfig vesselSpawnConfig = null) + { + if (vesselSpawnConfig != null) + { + SelectedCrewMembers.Remove(vesselSpawnConfig.kerbalName); + vesselSpawnConfig.kerbalName = null; + } + showCrewSelection = false; + currentVesselSpawnConfig = null; + GUIUtils.SetGUIRectVisible(_crewGUICheckIndex, false); + } + + /// + /// Crew selection window borrowed from VesselMover and modified. + /// + /// + public void CrewSelectionWindow(int windowID) + { + KerbalRoster kerbalRoster = HighLogic.CurrentGame.CrewRoster; + GUI.DragWindow(new Rect(0, 0, crewSelectionWindowRect.width, 20)); + GUILayout.BeginVertical(); + crewSelectionScrollPos = GUILayout.BeginScrollView(crewSelectionScrollPos, GUI.skin.box, GUILayout.Width(crewSelectionWindowRect.width - 15), GUILayout.MaxHeight(crewSelectionWindowRect.height - 60)); + using (var kerbals = kerbalRoster.Kerbals(ProtoCrewMember.KerbalType.Crew).GetEnumerator()) + while (kerbals.MoveNext()) + { + ProtoCrewMember crewMember = kerbals.Current; + if (crewMember == null || SelectedCrewMembers.Contains(crewMember.name) || ObserverCrewMembers.Contains(crewMember.name) || ActiveCrewMembers.Contains(crewMember.name)) continue; + if (GUILayout.Button($"{crewMember.name}, {crewMember.gender}, {crewMember.trait}", BDArmorySetup.BDGuiSkin.button)) + { + SelectedCrewMembers.Remove(currentVesselSpawnConfig.kerbalName); + SelectedCrewMembers.Add(crewMember.name); + currentVesselSpawnConfig.kerbalName = crewMember.name; + HideCrewSelection(); + } + } + GUILayout.EndScrollView(); + GUILayout.Space(10); + GUILayout.BeginHorizontal(); + if (GUILayout.Button(StringUtils.Localize("#LOC_BDArmory_Settings_CustomSpawnTemplate_Clear"), BDArmorySetup.BDGuiSkin.button)) + { + SelectedCrewMembers.Remove(currentVesselSpawnConfig.kerbalName); + currentVesselSpawnConfig.kerbalName = null; + HideCrewSelection(); + } + if (GUILayout.Button(StringUtils.Localize("#LOC_BDArmory_Settings_CustomSpawnTemplate_ClearAll"), BDArmorySetup.BDGuiSkin.button)) + { + SelectedCrewMembers.Clear(); + foreach (var team in customSpawnConfig.customVesselSpawnConfigs) + foreach (var member in team) + member.kerbalName = null; + } + if (GUILayout.Button(StringUtils.Localize("#LOC_BDArmory_Settings_CustomSpawnTemplate_Refresh"), BDArmorySetup.BDGuiSkin.button)) + { RefreshSelectedCrew(); } + GUILayout.EndHorizontal(); + GUILayout.EndVertical(); + GUIUtils.RepositionWindow(ref crewSelectionWindowRect); + GUIUtils.UpdateGUIRect(crewSelectionWindowRect, _crewGUICheckIndex); + GUIUtils.UseMouseEventInRect(crewSelectionWindowRect); + if (bringCrewSelectionToFront) + { + bringCrewSelectionToFront = false; + GUI.BringWindowToFront(windowID); + } + } + + /// + /// Refresh the list of who's been selected. + /// + void RefreshSelectedCrew() + { + SelectedCrewMembers.Clear(); + foreach (var team in customSpawnConfig.customVesselSpawnConfigs) + foreach (var member in team) + if (!string.IsNullOrEmpty(member.kerbalName)) + SelectedCrewMembers.Add(member.kerbalName); + } + + /// + /// Refresh the crew members that are on observer craft. + /// + public void RefreshObserverCrewMembers() + { + ObserverCrewMembers.Clear(); + // Find any crew on observer vessels. + foreach (var vessel in VesselSpawnerWindow.Instance.Observers) + { + if (vessel == null || !vessel.loaded) continue; + foreach (var part in vessel.Parts) + { + if (part == null) continue; + foreach (var crew in part.protoModuleCrew) + { + if (crew == null) continue; + ObserverCrewMembers.Add(crew.name); + } + } + } + // Remove any observers from already assigned slots. + foreach (var team in customSpawnConfig.customVesselSpawnConfigs) + foreach (var member in team) + if (!string.IsNullOrEmpty(member.kerbalName) && ObserverCrewMembers.Contains(member.kerbalName)) + member.kerbalName = null; + // Then refresh the selected crew. + RefreshSelectedCrew(); + } + #endregion + + /// + /// Hide other custom spawn template windows except for the named one. + /// + /// The window to keep open. + public void HideOtherWindows(string keep) + { + if (showTemplateSelection && keep != "template") HideTemplateSelection(); + if (showCrewSelection && keep != "crew") HideCrewSelection(); + if (showVesselSelection && keep != "vessel") HideVesselSelection(); + } + + #endregion + } + + [AttributeUsage(AttributeTargets.Field)] + public class CustomSpawnTemplateField : Attribute + { + public static string customSpawnTemplateFileLocation = Path.GetFullPath(Path.Combine(KSPUtil.ApplicationRootPath, "GameData", "BDArmory", "PluginData", "spawn_templates.cfg")); + + /// + /// Save the custom spawn templates to disk. + /// + public static void Save() + { + ConfigNode fileNode = ConfigNode.Load(customSpawnTemplateFileLocation); + if (fileNode == null) + fileNode = new ConfigNode(); + + if (!fileNode.HasNode("CustomSpawnTemplates")) + fileNode.AddNode("CustomSpawnTemplates"); + + ConfigNode spawnTemplates = fileNode.GetNode("CustomSpawnTemplates"); + + spawnTemplates.ClearNodes(); + foreach (var spawnTemplate in CustomTemplateSpawning.customSpawnConfigs) + { + if (string.IsNullOrEmpty(spawnTemplate.name) || spawnTemplate.customVesselSpawnConfigs.Count == 0) continue; // Skip unnamed or invalid templates. + + var templateNode = spawnTemplates.AddNode("TEMPLATE"); + templateNode.AddValue("name", spawnTemplate.name); + templateNode.AddValue("worldIndex", spawnTemplate.worldIndex); + templateNode.AddValue("latitude", spawnTemplate.latitude); + templateNode.AddValue("longitude", spawnTemplate.longitude); + templateNode.AddValue("altitude", spawnTemplate.altitude); + foreach (var team in spawnTemplate.customVesselSpawnConfigs) + { + var teamNode = templateNode.AddNode("TEAM"); + foreach (var member in team) + { + var memberNode = teamNode.AddNode("MEMBER"); + memberNode.AddValue("latitude", member.latitude); + memberNode.AddValue("longitude", member.longitude); + memberNode.AddValue("heading", member.heading); + } + } + } + + if (!Directory.GetParent(customSpawnTemplateFileLocation).Exists) + { Directory.GetParent(customSpawnTemplateFileLocation).Create(); } + fileNode.Save(customSpawnTemplateFileLocation); + } + + /// + /// Load the custom spawn templates from disk. + /// + public static void Load() + { + ConfigNode fileNode = ConfigNode.Load(customSpawnTemplateFileLocation); + CustomTemplateSpawning.customSpawnConfigs = new List(); + if (fileNode != null) + { + if (fileNode.HasNode("CustomSpawnTemplates")) + { + ConfigNode spawnTemplates = fileNode.GetNode("CustomSpawnTemplates"); + foreach (var templateNode in spawnTemplates.GetNodes("TEMPLATE")) + { + try + { + var customSpawnConfig = new CustomSpawnConfig( + (string)ParseField(templateNode, "name", typeof(string)), + new SpawnConfig( + worldIndex: (int)ParseField(templateNode, "worldIndex", typeof(int)), + latitude: (float)ParseField(templateNode, "latitude", typeof(float)), + longitude: (float)ParseField(templateNode, "longitude", typeof(float)), + altitude: (float)ParseField(templateNode, "altitude", typeof(float)) + ), + new List>() + ); + int teamCount = 0; + foreach (var teamNode in templateNode.GetNodes("TEAM")) + { + if (teamNode == null) continue; + var team = new List(); + foreach (var memberNode in teamNode.GetNodes("MEMBER")) + { + if (memberNode == null) continue; + team.Add(new CustomVesselSpawnConfig( + latitude: (double)ParseField(memberNode, "latitude", typeof(double)), + longitude: (double)ParseField(memberNode, "longitude", typeof(double)), + heading: (float)ParseField(memberNode, "heading", typeof(float)), + teamIndex: teamCount + )); + } + if (team.Count > 0) + customSpawnConfig.customVesselSpawnConfigs.Add(team); + ++teamCount; + } + if (customSpawnConfig.customVesselSpawnConfigs.Count() > 0) + CustomTemplateSpawning.customSpawnConfigs.Add(customSpawnConfig); + } + catch (Exception e) + { + Debug.LogException(e); + } + } + } + } + } + + /// + /// Try to parse the named field from the config node as the specified type. + /// + /// The config node + /// The field name + /// The type to parse as + /// The value as an object or null + private static object ParseField(ConfigNode node, string field, Type type) + { + try + { + if (!node.HasValue(field)) + { + throw new ArgumentNullException(field, $"Field '{field}' is missing."); + } + var value = node.GetValue(field); + try + { + if (type == typeof(string)) + { return value; } + else if (type == typeof(bool)) + { return bool.Parse(value); } + else if (type == typeof(int)) + { return int.Parse(value); } + else if (type == typeof(float)) + { return float.Parse(value); } + else if (type == typeof(double)) + { return double.Parse(value); } + else + { throw new ArgumentException("Invalid type specified."); } + } + catch (Exception e) + { throw new ArgumentException($"Field '{field}': '{value}' could not be parsed as '{type}' | {e.ToString()}", field); } + } + catch (Exception e) + { + Debug.LogException(e); + } + Debug.LogError($"[BDArmory.CustomSpawnTemplate]: Failed to parse field '{field}' of type '{type}' on node '{node.name}'"); + return null; + } + } +} diff --git a/BDArmory/VesselSpawning/SingleVesselSpawning.cs b/BDArmory/VesselSpawning/SingleVesselSpawning.cs new file mode 100644 index 000000000..24f6521f7 --- /dev/null +++ b/BDArmory/VesselSpawning/SingleVesselSpawning.cs @@ -0,0 +1,98 @@ +using UnityEngine; +using System.Collections; +using System.IO; +using System.Linq; + +using BDArmory.Competition; +using BDArmory.Extensions; +using BDArmory.Utils; + +namespace BDArmory.VesselSpawning +{ + [KSPAddon(KSPAddon.Startup.Flight, false)] + public class SingleVesselSpawning : VesselSpawnerBase + { + public static SingleVesselSpawning Instance; + + protected override void Awake() + { + base.Awake(); + if (Instance != null) Destroy(Instance); + Instance = this; + } + + void LogMessage(string message, bool toScreen = true, bool toLog = true) => LogMessageFrom("SingleVesselSpawning", message, toScreen, toLog); + + public override IEnumerator Spawn(SpawnConfig spawnConfig) + { + if (spawnConfig.craftFiles == null || spawnConfig.craftFiles.Count == 0) + { + var spawnFolder = Path.Combine(AutoSpawnPath, spawnConfig.folder); + spawnConfig.craftFiles = Directory.GetFiles(spawnFolder, "*.craft").ToList(); + if (spawnConfig.craftFiles.Count == 0) + { + LogMessage($"No craft files found in {spawnFolder}, aborting."); + spawnFailureReason = SpawnFailureReason.NoCraft; + vesselsSpawning = false; + yield break; + } + } + PreSpawnInitialisation(spawnConfig); + yield return SpawnVessel(spawnConfig.craftFiles.First(), spawnConfig.latitude, spawnConfig.longitude, spawnConfig.altitude); // FIXME This lacks initialHeading and initialPitch. Really, this should be converted to use a VesselSpawnConfig instead and the spawnConfig for the PreSpawnInitialisation generated from it. + vesselsSpawning = false; + } + + public override void PreSpawnInitialisation(SpawnConfig spawnConfig) + { + base.PreSpawnInitialisation(spawnConfig); + + vesselsSpawning = true; // Signal that we've started the spawning vessels routine. + vesselSpawnSuccess = false; // Set our success flag to false for now. + spawnFailureReason = SpawnFailureReason.None; // Reset the spawn failure reason. + } + + public IEnumerator SpawnVessel(string craftUrl, double latitude, double longitude, double altitude, float initialHeading = 90f, float initialPitch = 0f) + { + // Convert the parameters to a VesselSpawnConfig. + var terrainAltitude = FlightGlobals.currentMainBody.TerrainAltitude(latitude, longitude); + var spawnPoint = FlightGlobals.currentMainBody.GetWorldSurfacePosition(latitude, longitude, terrainAltitude + altitude); + var radialUnitVector = (spawnPoint - FlightGlobals.currentMainBody.transform.position).normalized; + var north = VectorUtils.GetNorthVector(spawnPoint, FlightGlobals.currentMainBody); + var direction = (Quaternion.AngleAxis(initialHeading, radialUnitVector) * north).ProjectOnPlanePreNormalized(radialUnitVector).normalized; + var airborne = altitude > 10; + VesselSpawnConfig vesselSpawnConfig = new VesselSpawnConfig(craftUrl, spawnPoint, direction, (float)altitude, initialPitch, airborne); + + // Spawn vessel. + yield return SpawnSingleVessel(vesselSpawnConfig); + if (spawnFailureReason != SpawnFailureReason.None) yield break; + var vessel = spawnedVessels[latestSpawnedVesselName]; + if (vessel == null) + { + spawnFailureReason = SpawnFailureReason.VesselFailedToSpawn; + yield break; + } + var vesselName = vessel.vesselName; + + // Perform the standard post-spawn main sequence. + yield return PostSpawnMainSequence(vessel, airborne); + if (spawnFailureReason != SpawnFailureReason.None) yield break; + + // If a competition is active, add them to it. + if (BDACompetitionMode.Instance.competitionIsActive || BDACompetitionMode.Instance.competitionStarting) + { + // Note: it's more complicated to add craft to a competition that is starting, but not started yet, so either add them before starting, or wait until it's started. + yield return new WaitWhile(() => BDACompetitionMode.Instance.competitionStarting); + if (vessel == null) + { + LogMessage(vesselName + " disappeared while waiting for the competition to start!"); + spawnFailureReason = SpawnFailureReason.VesselLostParts; + yield break; + } + + AddToActiveCompetition(vessel, airborne); + } + + vesselSpawnSuccess = true; + } + } +} \ No newline at end of file diff --git a/BDArmory/VesselSpawning/SpawnConfig.cs b/BDArmory/VesselSpawning/SpawnConfig.cs new file mode 100644 index 000000000..fe80eb3ba --- /dev/null +++ b/BDArmory/VesselSpawning/SpawnConfig.cs @@ -0,0 +1,150 @@ +using System; +using System.IO; +using System.Linq; +using System.Collections.Generic; +using UnityEngine; + +namespace BDArmory.VesselSpawning +{ + /// + /// Configuration for spawning groups of vessels. + /// + /// Note: + /// This is currently partially specific to SpawnAllVesselsOnce and SpawnVesselsContinuosly. + /// TODO: Make this generic and make CircularSpawnConfig a derived class of this. + /// + [Serializable] + public class SpawnConfig + { + public SpawnConfig(int worldIndex, double latitude, double longitude, double altitude, bool killEverythingFirst = true, bool assignTeams = true, int numberOfTeams = 0, List teamCounts = null, List> teamsSpecific = null, string folder = "", List craftFiles = null) + { + this.worldIndex = worldIndex; + this.latitude = latitude; + this.longitude = longitude; + this.altitude = altitude; + this.killEverythingFirst = killEverythingFirst; + this.assignTeams = assignTeams; + this.numberOfTeams = numberOfTeams; + this.teamCounts = teamCounts; if (teamCounts != null) this.numberOfTeams = this.teamCounts.Count; + this.teamsSpecific = teamsSpecific; + this.folder = folder ?? ""; + this.craftFiles = craftFiles; + } + public SpawnConfig(SpawnConfig other) + { + this.worldIndex = other.worldIndex; + this.latitude = other.latitude; + this.longitude = other.longitude; + this.altitude = other.altitude; + this.killEverythingFirst = other.killEverythingFirst; + this.assignTeams = other.assignTeams; + this.numberOfTeams = other.numberOfTeams; + this.teamCounts = other.teamCounts; + this.teamsSpecific = other.teamsSpecific; + this.folder = other.folder; + this.craftFiles = other.craftFiles?.ToList(); + } + public int worldIndex; + public double latitude; + public double longitude; + public double altitude; + public bool killEverythingFirst = true; + public bool assignTeams = true; + public int numberOfTeams = 0; // Number of teams (or FFA, Folders or Inf). For evenly (as possible) splitting vessels into teams. + public List teamCounts; // List of team numbers. For unevenly splitting vessels into teams based on their order in the tournament state file for the round. E.g., when spawning from folders. + public List> teamsSpecific; // Dictionary of vessels and teams. For splitting specific vessels into specific teams. + public string folder = ""; + public List craftFiles = null; + } + + /// + /// Configuration for spawning individual vessels. + /// @Note: this has to be a class so that setting editorFacility during spawning persists back to the calling function. + /// + [Serializable] + public class VesselSpawnConfig + { + public string craftURL; // The craft file. + public Vector3 position; // World-space coordinates (x,y,z) to place the vessel once spawned (before adjusting for terrain altitude). + public Vector3 direction; // Direction to point the plane horizontally (i.e., heading). + public float altitude; // Altitude above terrain / water to adjust spawning position to. + public float pitch; // Pitch if spawning airborne. + public bool airborne; // Whether the vessel should be spawned in an airborne configuration or not. + public int teamIndex; + public bool reuseURLVesselName; // Reuse the vesselName for the same craftURL (for continuous spawning). + public List crew; // Override the crew. + public EditorFacility editorFacility = EditorFacility.SPH; // Which editorFacility the craft belongs to (found out during spawning). + public VesselSpawnConfig(string craftURL, Vector3 position, Vector3 direction, float altitude, float pitch, bool airborne, int teamIndex = 0, bool reuseURLVesselName = false, List crew = null) + { + this.craftURL = craftURL; + this.position = position; + this.direction = direction; + this.altitude = altitude; + this.pitch = pitch; + this.airborne = airborne; + this.teamIndex = teamIndex; + this.reuseURLVesselName = reuseURLVesselName; + this.crew = crew == null ? null : crew.ToList(); // Take a copy. + } + } + + /// + /// Spawn config for circular spawning. + /// Probably more of the fields from SpawnConfig should be in here. + /// + [Serializable] + public class CircularSpawnConfig : SpawnConfig + { + public CircularSpawnConfig(SpawnConfig spawnConfig, float distance, bool absDistanceOrFactor) : base(spawnConfig) + { + this.distance = distance; + this.absDistanceOrFactor = absDistanceOrFactor; + } + public CircularSpawnConfig(CircularSpawnConfig other) : base(other) + { + this.distance = other.distance; + this.absDistanceOrFactor = other.absDistanceOrFactor; + } + public CircularSpawnConfig(int worldIndex, double latitude, double longitude, double altitude, float distance, bool absDistanceOrFactor, bool killEverythingFirst = true, bool assignTeams = true, int numberOfTeams = 0, List teamCounts = null, List> teamsSpecific = null, string folder = "", List craftFiles = null) : this(new SpawnConfig(worldIndex, latitude, longitude, altitude, killEverythingFirst, assignTeams, numberOfTeams, teamCounts, teamsSpecific, folder, craftFiles), distance, absDistanceOrFactor) { } // Constructor for legacy SpawnConfigs that should be CircularSpawnConfigs. + public float distance; + public bool absDistanceOrFactor; // If true, the distance value is used as-is, otherwise it is used as a factor giving the actual distance: (N+1)*distance, where N is the number of vessels. + } + + /// + /// Spawn config for custom templates. + /// + [Serializable] + public class CustomSpawnConfig : SpawnConfig + { + public CustomSpawnConfig(string name, SpawnConfig spawnConfig, List> vesselSpawnConfigs) : base(spawnConfig) + { + this.name = name; + this.customVesselSpawnConfigs = vesselSpawnConfigs; + } + public string name; + public List> customVesselSpawnConfigs; + public override string ToString() => $"{{name: {name}, worldIndex: {worldIndex}, lat: {latitude:F3}, lon: {longitude:F3}, alt: {altitude:F0}; {(customVesselSpawnConfigs == null ? "" : string.Join("; ", customVesselSpawnConfigs.Select(cfgs => string.Join(", ", cfgs))))}}}"; + } + + /// + /// The individual custom vessel spawn configs. + /// + [Serializable] + public class CustomVesselSpawnConfig + { + public CustomVesselSpawnConfig(double latitude, double longitude, float heading, int teamIndex) + { + this.latitude = latitude; + this.longitude = longitude; + this.heading = heading; + this.teamIndex = teamIndex; + } + public string craftURL; + public string kerbalName; + public double latitude; + public double longitude; + public float heading; + public int teamIndex; + public override string ToString() => $"{{{(string.IsNullOrEmpty(craftURL) ? "" : $"{Path.GetFileNameWithoutExtension(craftURL)}, ")}{(string.IsNullOrEmpty(kerbalName) ? "" : $"{kerbalName}, ")}lat: {latitude:G3}, lon: {longitude:G3}, heading: {heading:F0}°, team: {teamIndex}}}"; + } +} \ No newline at end of file diff --git a/BDArmory/Competition/VesselSpawning/SpawnLocations.cs b/BDArmory/VesselSpawning/SpawnLocations.cs similarity index 98% rename from BDArmory/Competition/VesselSpawning/SpawnLocations.cs rename to BDArmory/VesselSpawning/SpawnLocations.cs index f8fecca52..3182646fa 100644 --- a/BDArmory/Competition/VesselSpawning/SpawnLocations.cs +++ b/BDArmory/VesselSpawning/SpawnLocations.cs @@ -4,7 +4,7 @@ using UniLinq; using UnityEngine; -namespace BDArmory.Competition.VesselSpawning +namespace BDArmory.VesselSpawning { [KSPAddon(KSPAddon.Startup.Flight, false)] public class SpawnLocations : MonoBehaviour @@ -77,12 +77,12 @@ public VesselSpawnerField() { } new SpawnLocation("Marshlands", new Vector2d(16.83, -162.813), 1), new SpawnLocation("Mountain Bowl", new Vector2d(21.772, -112.569), 1), new SpawnLocation("Mtn. Springs", new Vector2d(30.6516, -40.6589), 1), - new SpawnLocation("Oasis", new Vector2d(10.5383, -121.837), 1), + new SpawnLocation("Oasis", new Vector2d(10.3, -121.2), 1), new SpawnLocation("Oyster Bay", new Vector2d(8.342, 85.613), 1), new SpawnLocation("Penninsula", new Vector2d(-1.2664, -106.896), 1), new SpawnLocation("Pyramids", new Vector2d(-6.4743, -141.662), 1), new SpawnLocation("Src of deNile", new Vector2d(28.8112, -134.795), 1), - new SpawnLocation("Suez", new Vector2d(10.955, -96.9358), 1), + new SpawnLocation("Suez", new Vector2d(10.6178, -97.0315), 1), new SpawnLocation("The Scar", new Vector2d(16.88, 50.48), 1), new SpawnLocation("Western Approach", new Vector2d(0.2, -84.26), 1), new SpawnLocation("White Cliffs", new Vector2d(25.689, -144.14), 1), diff --git a/BDArmory/Competition/SpawnStrategies/CircularSpawnStrategy.cs b/BDArmory/VesselSpawning/SpawnStrategies/CircularSpawnStrategy.cs similarity index 73% rename from BDArmory/Competition/SpawnStrategies/CircularSpawnStrategy.cs rename to BDArmory/VesselSpawning/SpawnStrategies/CircularSpawnStrategy.cs index b384fcd79..025c53fad 100644 --- a/BDArmory/Competition/SpawnStrategies/CircularSpawnStrategy.cs +++ b/BDArmory/VesselSpawning/SpawnStrategies/CircularSpawnStrategy.cs @@ -1,13 +1,12 @@ -using System; -using System.Collections; +using System.Collections; using System.Collections.Generic; using System.Linq; -using BDArmory.Competition.VesselSpawning; using UnityEngine; + using BDArmory.Competition.RemoteOrchestration; -using static BDArmory.Competition.VesselSpawning.VesselSpawner; +using BDArmory.Settings; -namespace BDArmory.Competition.SpawnStrategies +namespace BDArmory.VesselSpawning.SpawnStrategies { public class CircularSpawnStrategy : SpawnStrategy { @@ -31,19 +30,22 @@ public CircularSpawnStrategy(VesselSource vesselSource, List vesselIds, int this.radius = radius; } - public IEnumerator Spawn(VesselSpawner spawner) + public IEnumerator Spawn(VesselSpawnerBase spawner) { // use vesselSource to resolve local paths for active vessels var craftUrls = vesselIds.Select(e => vesselSource.GetLocalPath(e)); // spawn all craftUrls in a circle around the center point - SpawnConfig spawnConfig = new SpawnConfig( - bodyIndex, - latitude, - longitude, - altitude, + CircularSpawnConfig spawnConfig = new CircularSpawnConfig( + new SpawnConfig( + bodyIndex, + latitude, + longitude, + altitude, + true, + craftFiles: new List(craftUrls) + ), radius, - true, - craftFiles: new List(craftUrls) + BDArmorySettings.VESSEL_SPAWN_DISTANCE_TOGGLE ); yield return spawner.Spawn(spawnConfig); diff --git a/BDArmory/Competition/SpawnStrategies/ListSpawnStrategy.cs b/BDArmory/VesselSpawning/SpawnStrategies/ListSpawnStrategy.cs similarity index 76% rename from BDArmory/Competition/SpawnStrategies/ListSpawnStrategy.cs rename to BDArmory/VesselSpawning/SpawnStrategies/ListSpawnStrategy.cs index fd6aeca60..6d586e488 100644 --- a/BDArmory/Competition/SpawnStrategies/ListSpawnStrategy.cs +++ b/BDArmory/VesselSpawning/SpawnStrategies/ListSpawnStrategy.cs @@ -1,9 +1,7 @@ -using System; -using System.Collections; +using System.Collections; using System.Collections.Generic; -using BDArmory.Competition.VesselSpawning; -namespace BDArmory.Competition.SpawnStrategies +namespace BDArmory.VesselSpawning.SpawnStrategies { public class ListSpawnStrategy : SpawnStrategy { @@ -20,7 +18,7 @@ public bool DidComplete() return success; } - public IEnumerator Spawn(VesselSpawner spawner) + public IEnumerator Spawn(VesselSpawnerBase spawner) { success = false; foreach (var item in strategies) diff --git a/BDArmory/Competition/SpawnStrategies/PointSpawnStrategy.cs b/BDArmory/VesselSpawning/SpawnStrategies/PointSpawnStrategy.cs similarity index 88% rename from BDArmory/Competition/SpawnStrategies/PointSpawnStrategy.cs rename to BDArmory/VesselSpawning/SpawnStrategies/PointSpawnStrategy.cs index f315dd72f..73f21efe1 100644 --- a/BDArmory/Competition/SpawnStrategies/PointSpawnStrategy.cs +++ b/BDArmory/VesselSpawning/SpawnStrategies/PointSpawnStrategy.cs @@ -1,11 +1,10 @@ using System.Collections; using System.Collections.Generic; -using BDArmory.Competition.VesselSpawning; using UnityEngine; -using BDArmory.Core; +using BDArmory.Settings; -namespace BDArmory.Competition.SpawnStrategies +namespace BDArmory.VesselSpawning.SpawnStrategies { public class PointSpawnStrategy : SpawnStrategy { @@ -24,7 +23,7 @@ public PointSpawnStrategy(string craftUrl, double latitude, double longitude, do this.pitch = pitch; } - public IEnumerator Spawn(VesselSpawner spawner) + public IEnumerator Spawn(VesselSpawnerBase spawner) { Debug.Log("[BDArmory.BDAScoreService] PointSpawnStrategy spawning."); @@ -39,7 +38,7 @@ public IEnumerator Spawn(VesselSpawner spawner) // Essentially, the differences in the spawning strategies are so large, that I don't think the currently defined interface is really suitable. // One option would be to remove the "VesselSpawner spawner" from the "public IEnumerator Spawn(VesselSpawner spawner);" in SpawnStrategy.cs and get the appropriate vessel spawner instance directly in each SpawnStrategy.Spawn function, which would then call the specific spawning functions of the vessel spawner instead of "spawner.Spawn(spawnConfig)" as below. // E.g., yield return SingleVesselSpawning.Instance.SpawnVessel(craftUrl, latitude, longitude, altitude, heading, pitch); - yield return spawner.Spawn(new SpawnConfig(worldIndex, latitude, longitude, altitude, 0, false, BDArmorySettings.VESSEL_SPAWN_EASE_IN_SPEED, false, false, 0, null, null, "", new List{craftUrl})); + yield return spawner.Spawn(new SpawnConfig(worldIndex, latitude, longitude, altitude, false, false, 0, null, null, "", new List{craftUrl})); // wait for spawner to finish yield return new WaitWhile(() => spawner.vesselsSpawning); diff --git a/BDArmory/Competition/SpawnStrategies/SpawnConfigStrategy.cs b/BDArmory/VesselSpawning/SpawnStrategies/SpawnConfigStrategy.cs similarity index 84% rename from BDArmory/Competition/SpawnStrategies/SpawnConfigStrategy.cs rename to BDArmory/VesselSpawning/SpawnStrategies/SpawnConfigStrategy.cs index c53a7a961..620b8dd3d 100644 --- a/BDArmory/Competition/SpawnStrategies/SpawnConfigStrategy.cs +++ b/BDArmory/VesselSpawning/SpawnStrategies/SpawnConfigStrategy.cs @@ -1,9 +1,7 @@ using System.Collections; using UnityEngine; -using BDArmory.Competition.VesselSpawning; - -namespace BDArmory.Competition.SpawnStrategies +namespace BDArmory.VesselSpawning.SpawnStrategies { /// /// A simple pass-through strategy to be able to use the current spawning functions properly. @@ -15,7 +13,7 @@ public class SpawnConfigStrategy : SpawnStrategy public SpawnConfigStrategy(SpawnConfig spawnConfig) { this.spawnConfig = spawnConfig; } - public IEnumerator Spawn(VesselSpawner spawner) + public IEnumerator Spawn(VesselSpawnerBase spawner) { yield return spawner.Spawn(spawnConfig); diff --git a/BDArmory/Competition/SpawnStrategies/SpawnStrategy.cs b/BDArmory/VesselSpawning/SpawnStrategies/SpawnStrategy.cs similarity index 64% rename from BDArmory/Competition/SpawnStrategies/SpawnStrategy.cs rename to BDArmory/VesselSpawning/SpawnStrategies/SpawnStrategy.cs index 59b86c18c..547ec7642 100644 --- a/BDArmory/Competition/SpawnStrategies/SpawnStrategy.cs +++ b/BDArmory/VesselSpawning/SpawnStrategies/SpawnStrategy.cs @@ -1,8 +1,6 @@ -using System; -using System.Collections; -using BDArmory.Competition.VesselSpawning; +using System.Collections; -namespace BDArmory.Competition.SpawnStrategies +namespace BDArmory.VesselSpawning.SpawnStrategies { public interface SpawnStrategy { @@ -13,7 +11,7 @@ public interface SpawnStrategy /// /// /// - public IEnumerator Spawn(VesselSpawner spawner); + public IEnumerator Spawn(VesselSpawnerBase spawner); public bool DidComplete(); } diff --git a/BDArmory/VesselSpawning/SpawnUtils.cs b/BDArmory/VesselSpawning/SpawnUtils.cs new file mode 100644 index 000000000..a98cca738 --- /dev/null +++ b/BDArmory/VesselSpawning/SpawnUtils.cs @@ -0,0 +1,753 @@ +using UnityEngine; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +using BDArmory.Competition; +using BDArmory.Control; +using BDArmory.Extensions; +using BDArmory.GameModes; +using BDArmory.Modules; +using BDArmory.Settings; +using BDArmory.UI; +using BDArmory.Utils; +using BDArmory.Weapons.Missiles; + +namespace BDArmory.VesselSpawning +{ + public enum SpawnFailureReason { None, NoCraft, NoTerrain, InvalidVessel, VesselLostParts, VesselFailedToSpawn, TimedOut, Cancelled, DependencyIssues }; + + public static class SpawnUtils + { + // Cancel all spawning modes. + public static void CancelSpawning() + { + VesselSpawnerStatus.spawnFailureReason = SpawnFailureReason.Cancelled; + + // Single spawn + if (CircularSpawning.Instance.vesselsSpawning || CircularSpawning.Instance.vesselsSpawningOnceContinuously) + { CircularSpawning.Instance.CancelSpawning(); } + + // Continuous spawn + if (ContinuousSpawning.Instance.vesselsSpawningContinuously) + { ContinuousSpawning.Instance.CancelSpawning(); } + + SpawnUtils.RevertSpawnLocationCamera(true); + } + + /// + /// If the VESSELNAMING tag exists in the craft file, then KSP renames the vessel at some point after spawning. + /// This function checks for renamed vessels and sets the name back to what it was. + /// This must be called once after a yield, before using vessel.vesselName as an index in spawnedVessels.Keys. + /// + /// + public static void CheckForRenamedVessels(Dictionary vessels) + { + foreach (var vesselName in vessels.Keys.ToList()) + { + if (vesselName != vessels[vesselName].vesselName) + { + vessels[vesselName].vesselName = vesselName; + } + } + } + + public static int PartCount(Vessel vessel, bool ignoreEVA = true) + { + if (vessel == null) return 0; + if (!ignoreEVA) return vessel.parts.Count; + int count = 0; + using (var part = vessel.parts.GetEnumerator()) + while (part.MoveNext()) + { + if (part.Current == null) continue; + if (ignoreEVA && part.Current.IsKerbalEVA()) continue; // Ignore EVA kerbals, which get added at some point after spawning. + ++count; + } + return count; + } + + public static Dictionary PartCrewCounts + { + get + { + if (_partCrewCounts == null) + { + _partCrewCounts = new Dictionary(); + foreach (var part in PartLoader.LoadedPartsList) + { + if (part == null || part.partPrefab == null || part.partPrefab.CrewCapacity < 1) continue; + if (BDArmorySettings.DEBUG_SPAWNING) Debug.Log($"[BDArmory.SpawnUtils]: {part.name} has crew capacity {part.partPrefab.CrewCapacity}."); + if (!_partCrewCounts.ContainsKey(part.name)) + { _partCrewCounts.Add(part.name, part.partPrefab.CrewCapacity); } + else // Duplicate part name! + { + if (part.partPrefab.CrewCapacity != _partCrewCounts[part.name]) + { + Debug.LogWarning($"[BDArmory.SpawnUtils]: Found a duplicate part {part.name} with a different crew capacity! {_partCrewCounts[part.name]} vs {part.partPrefab.CrewCapacity}, using the minimum."); + _partCrewCounts[part.name] = Mathf.Min(_partCrewCounts[part.name], part.partPrefab.CrewCapacity); + } + else + { + Debug.LogWarning($"[BDArmory.SpawnUtils]: Found a duplicate part {part.name} with the same crew capacity!"); + } + } + } + } + return _partCrewCounts; + } + } + static Dictionary _partCrewCounts; + + #region Camera + public static void ShowSpawnPoint(int worldIndex, double latitude, double longitude, double altitude = 0, float distance = 0, bool spawning = false) => SpawnUtilsInstance.Instance.ShowSpawnPoint(worldIndex, latitude, longitude, altitude, distance, spawning); // Note: this may launch a coroutine when not spawning and there's no active vessel! + public static void RevertSpawnLocationCamera(bool keepTransformValues = true, bool revertIfDead = false) => SpawnUtilsInstance.Instance.RevertSpawnLocationCamera(keepTransformValues, revertIfDead); + #endregion + + #region Teams + public static Dictionary originalTeams = new Dictionary(); + public static void SaveTeams() + { + originalTeams.Clear(); + foreach (var weaponManager in LoadedVesselSwitcher.Instance.WeaponManagers.SelectMany(tm => tm.Value).ToList()) + { + originalTeams[weaponManager.vessel.vesselName] = weaponManager.Team.Name; + } + } + #endregion + + #region Engine Activation + public static int CountActiveEngines(Vessel vessel) + { + return VesselModuleRegistry.GetModules(vessel).Where(engine => engine.EngineIgnited).ToList().Count + FireSpitter.CountActiveEngines(vessel); + } + + public static void ActivateAllEngines(Vessel vessel, bool activate = true, bool ignoreModularMissileEngines = true) + { + foreach (var engine in VesselModuleRegistry.GetModules(vessel)) + { + if (ignoreModularMissileEngines && IsModularMissileEngine(engine)) continue; // Ignore modular missile engines. + if (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 55) engine.independentThrottle = false; + var mme = engine.part.FindModuleImplementing(); + if (mme == null) + { + if (activate) engine.Activate(); + else engine.Shutdown(); + } + else + { + if (mme.runningPrimary) + { + if (activate && !mme.PrimaryEngine.EngineIgnited) + { + mme.PrimaryEngine.Activate(); + } + else if (!activate && mme.PrimaryEngine.EngineIgnited) + { + mme.PrimaryEngine.Shutdown(); + } + } + else + { + if (activate && !mme.SecondaryEngine.EngineIgnited) + { + mme.SecondaryEngine.Activate(); + } + else if (!activate && mme.SecondaryEngine.EngineIgnited) + { + mme.SecondaryEngine.Shutdown(); + } + } + } + } + FireSpitter.ActivateFSEngines(vessel, activate); + } + + public static bool IsModularMissileEngine(ModuleEngines engine) + { + var part = engine.part; + if (part is not null) + { + var firstDecoupler = BDModularGuidance.FindFirstDecoupler(part.parent, null); + if (firstDecoupler is not null && HasMMGInChildren(firstDecoupler.part)) return true; + } + return false; + } + static bool HasMMGInChildren(Part part) + { + if (part is null) return false; + if (part.FindModuleImplementing() is not null) return true; + foreach (var child in part.children) + if (HasMMGInChildren(child)) return true; + return false; + } + #endregion + + #region Intake hacks + public static void HackIntakesOnNewVessels(bool enable) => SpawnUtilsInstance.Instance.HackIntakesOnNewVessels(enable); + public static void HackIntakes(Vessel vessel, bool enable) => SpawnUtilsInstance.Instance.HackIntakes(vessel, enable); + #endregion + + #region ControlSurface hacks + public static void HackActuatorsOnNewVessels(bool enable) => SpawnUtilsInstance.Instance.HackActuatorsOnNewVessels(enable); + public static void HackActuators(Vessel vessel, bool enable) => SpawnUtilsInstance.Instance.HackActuators(vessel, enable); + #endregion + + #region Space hacks + public static void SpaceFrictionOnNewVessels(bool enable) => SpawnUtilsInstance.Instance.SpaceFrictionOnNewVessels(enable); + public static void SpaceHacks(Vessel vessel) => SpawnUtilsInstance.Instance.SpaceHacks(vessel); + #endregion + + #region KAL + public static void RestoreKALGlobally(bool restore = true) { foreach (var vessel in FlightGlobals.VesselsLoaded) SpawnUtilsInstance.Instance.RestoreKAL(vessel, restore); } + public static void RestoreKAL(Vessel vessel, bool restore = true) => SpawnUtilsInstance.Instance.RestoreKAL(vessel, restore); + #endregion + + #region Post-Spawn + public static void OnVesselReady(Vessel vessel) => SpawnUtilsInstance.Instance.OnVesselReady(vessel); + #endregion + + #region Vessel Removal + public static bool removingVessels => SpawnUtilsInstance.Instance.removeVesselsPending > 0; + public static void RemoveVessel(Vessel vessel) => SpawnUtilsInstance.Instance.RemoveVessel(vessel); + public static IEnumerator RemoveAllVessels() => SpawnUtilsInstance.Instance.RemoveAllVessels(); + #endregion + + #region AI/WM stuff for RWP + public static bool CheckAIWMPlacement(Vessel vessel) + { + var message = ""; + List failureStrings = new List(); + var AI = VesselModuleRegistry.GetBDModulePilotAI(vessel, true); + var WM = VesselModuleRegistry.GetMissileFire(vessel, true); + if (AI == null) message = " has no AI"; + if (WM == null) message += (AI == null ? " or WM" : " has no WM"); + if (AI != null || WM != null) + { + int count = 0; + + if (AI != null && !(AI.part == AI.part.vessel.rootPart || AI.part.parent == AI.part.vessel.rootPart)) + { + message += (WM == null ? " and its AI" : "'s AI"); + ++count; + } + if (WM != null && !(WM.part == WM.part.vessel.rootPart || WM.part.parent == WM.part.vessel.rootPart)) + { + message += (AI == null ? " and its WM" : (count > 0 ? " and WM" : "'s WM")); + ++count; + }; + if (count > 0) message += (count > 1 ? " are" : " is") + " not attached to its root part"; + } + + if (!string.IsNullOrEmpty(message)) + { + message = $"{vessel.vesselName}" + message + "."; + BDACompetitionMode.Instance.competitionStatus.Add(message); + Debug.LogWarning("[BDArmory.SpawnUtils]: " + message); + return false; + } + return true; + } + + public static void CheckAIWMCounts(Vessel vessel) + { + var numberOfAIs = VesselModuleRegistry.GetModuleCount(vessel); + var numberOfWMs = VesselModuleRegistry.GetModuleCount(vessel); + string message = null; + if (numberOfAIs != 1 && numberOfWMs != 1) message = $"{vessel.vesselName} has {numberOfAIs} AIs and {numberOfWMs} WMs"; + else if (numberOfAIs != 1) message = $"{vessel.vesselName} has {numberOfAIs} AIs"; + else if (numberOfWMs != 1) message = $"{vessel.vesselName} has {numberOfWMs} WMs"; + if (message != null) + { + BDACompetitionMode.Instance.competitionStatus.Add(message); + Debug.LogWarning("[BDArmory.SpawnUtils]: " + message); + } + } + #endregion + } + + /// + /// Non-static MonoBehaviour version to be able to call coroutines. + /// + [KSPAddon(KSPAddon.Startup.Flight, false)] + public class SpawnUtilsInstance : MonoBehaviour + { + public static SpawnUtilsInstance Instance; + + void Awake() + { + if (Instance != null) Destroy(Instance); + Instance = this; + spawnLocationCamera = new GameObject("StationaryCameraParent"); + spawnLocationCamera = (GameObject)Instantiate(spawnLocationCamera, Vector3.zero, Quaternion.identity); + spawnLocationCamera.SetActive(false); + } + + void Start() + { + if (BDArmorySettings.HACK_INTAKES) HackIntakesOnNewVessels(true); + if (BDArmorySettings.SPACE_HACKS) SpaceFrictionOnNewVessels(true); + if (BDArmorySettings.RUNWAY_PROJECT) HackActuatorsOnNewVessels(true); + } + + void OnDestroy() + { + VesselSpawnerField.Save(); + Destroy(spawnLocationCamera); + HackIntakesOnNewVessels(false); + HackActuatorsOnNewVessels(false); + SpaceFrictionOnNewVessels(false); + } + + + #region Post-Spawn + public void OnVesselReady(Vessel vessel) => StartCoroutine(OnVesselReadyCoroutine(vessel)); + /// + /// Perform adjustments to spawned craft once they're loaded and unpacked. + /// + /// + IEnumerator OnVesselReadyCoroutine(Vessel vessel) + { + var wait = new WaitForFixedUpdate(); + while (vessel != null && (!vessel.loaded || vessel.packed)) yield return wait; + if (vessel == null) yield break; + // EVA Kerbals get their Assigned status reverted to Available for some reason. This fixes that. + foreach (var kerbal in VesselModuleRegistry.GetKerbalEVAs(vessel)) foreach (var crew in kerbal.part.protoModuleCrew) crew.rosterStatus = ProtoCrewMember.RosterStatus.Assigned; + } + #endregion + + #region Vessel Removal + public int removeVesselsPending = 0; + // Remove a vessel and clean up any remaining parts. This fixes the case where the currently focussed vessel refuses to die properly. + public void RemoveVessel(Vessel vessel) + { + if (vessel == null) return; + if (VesselSpawnerWindow.Instance.Observers.Contains(vessel)) return; // Don't remove observers. + if (BDArmorySettings.ASTEROID_RAIN && vessel.vesselType == VesselType.SpaceObject) return; // Don't remove asteroids we're using. + if (BDArmorySettings.ASTEROID_FIELD && vessel.vesselType == VesselType.SpaceObject) return; // Don't remove asteroids we're using. + StartCoroutine(RemoveVesselCoroutine(vessel)); + } + public IEnumerator RemoveVesselCoroutine(Vessel vessel) + { + if (vessel == null) yield break; + ++removeVesselsPending; + if (vessel != FlightGlobals.ActiveVessel && vessel.vesselType != VesselType.SpaceObject) + { + try + { + if (KerbalSafetyManager.Instance.safetyLevel != KerbalSafetyLevel.Off) + KerbalSafetyManager.Instance.RecoverVesselNow(vessel); + else + { + foreach (var part in vessel.Parts) part.OnJustAboutToBeDestroyed?.Invoke(); // Invoke any OnJustAboutToBeDestroyed events since RecoverVesselFromFlight calls DestroyImmediate, skipping the FX detachment triggers. + ShipConstruction.RecoverVesselFromFlight(vessel.protoVessel, HighLogic.CurrentGame.flightState, true); + } + } + catch (Exception e) + { + Debug.LogError($"[BDArmory.SpawnUtils]: Exception thrown while removing vessel: {e.Message}"); + } + } + else + { + if (vessel.vesselType == VesselType.SpaceObject) + { + if ((BDArmorySettings.ASTEROID_RAIN && AsteroidRain.IsManagedAsteroid(vessel)) + || (BDArmorySettings.ASTEROID_FIELD && AsteroidField.IsManagedAsteroid(vessel))) // Don't remove asteroids when we're using them. + { + --removeVesselsPending; + yield break; + } + if ((Versioning.version_major == 1 && Versioning.version_minor > 10) || Versioning.version_major > 1) // Comets introduced in 1.11 + RemoveComet_1_11(vessel); + } + vessel.Die(); // Kill the vessel + yield return waitForFixedUpdate; + if (vessel != null) + { + var partsToKill = vessel.parts.ToList(); // If it left any parts, kill them. (This occurs when the currently focussed vessel gets killed.) + foreach (var part in partsToKill) + part.Die(); + } + yield return waitForFixedUpdate; + } + --removeVesselsPending; + } + + void RemoveComet_1_11(Vessel vessel) + { + var cometVessel = vessel.FindVesselModuleImplementing(); + if (cometVessel) { Destroy(cometVessel); } + } + + /// + /// Remove all the vessels. + /// This works by spawning in a spawnprobe at the current camera coordinates so that we can clean up the other vessels properly. + /// + /// + public IEnumerator RemoveAllVessels() + { + var vesselsToKill = FlightGlobals.Vessels.ToList(); + // Spawn in the SpawnProbe at the camera position. + var spawnProbe = VesselSpawner.SpawnSpawnProbe(); + if (spawnProbe != null) // If the spawnProbe is null, then just try to kill everything anyway. + { + spawnProbe.Landed = false; // Tell KSP that it's not landed so KSP doesn't mess with its position. + yield return new WaitWhile(() => spawnProbe != null && (!spawnProbe.loaded || spawnProbe.packed)); + // Switch to the spawn probe. + while (spawnProbe != null && FlightGlobals.ActiveVessel != spawnProbe) + { + LoadedVesselSwitcher.Instance.ForceSwitchVessel(spawnProbe); + yield return waitForFixedUpdate; + } + } + // Kill all other vessels (including debris). + foreach (var vessel in vesselsToKill) + RemoveVessel(vessel); + // Finally, remove the SpawnProbe. + RemoveVessel(spawnProbe); + + // Now, clear the teams and wait for everything to be removed. + SpawnUtils.originalTeams.Clear(); + yield return new WaitWhile(() => removeVesselsPending > 0); + } + #endregion + + #region Camera Adjustment + GameObject spawnLocationCamera; + Transform originalCameraParentTransform; + float originalCameraNearClipPlane; + Coroutine delayedShowSpawnPointCoroutine; + private readonly WaitForFixedUpdate waitForFixedUpdate = new WaitForFixedUpdate(); + /// + /// Show the spawn point. + /// Note: When not spawning and there's no active vessel, this may launch a coroutine to perform the actual shift. + /// Note: If spawning is true, then the spawnLocationCamera takes over the camera and RevertSpawnLocationCamera should be called at some point to allow KSP to do its own camera stuff. + /// + /// The body the spawn point is on. + /// Latitude + /// Longitude + /// Altitude + /// Distance to view the point from. + /// Whether spawning is actually happening. + /// State parameter for when we need to spawn a probe first. + public void ShowSpawnPoint(int worldIndex, double latitude, double longitude, double altitude = 0, float distance = 0, bool spawning = false, bool recurse = true) + { + if (BDArmorySettings.ASTEROID_RAIN) { AsteroidRain.Instance.Reset(); } + if (BDArmorySettings.ASTEROID_FIELD) { AsteroidField.Instance.Reset(); } + if (!spawning && (FlightGlobals.ActiveVessel == null || FlightGlobals.ActiveVessel.state == Vessel.State.DEAD)) + { + if (!recurse) + { + Debug.LogWarning($"[BDArmory.SpawnUtils]: No active vessel, unable to show spawn point."); + return; + } + Debug.LogWarning($"[BDArmory.SpawnUtils]: Active vessel is dead or packed, spawning a new one."); + if (delayedShowSpawnPointCoroutine != null) { StopCoroutine(delayedShowSpawnPointCoroutine); delayedShowSpawnPointCoroutine = null; } + delayedShowSpawnPointCoroutine = StartCoroutine(DelayedShowSpawnPoint(worldIndex, latitude, longitude, altitude, distance, spawning)); + return; + } + var flightCamera = FlightCamera.fetch; + var cameraHeading = FlightCamera.CamHdg; + var cameraPitch = FlightCamera.CamPitch; + if (distance == 0) distance = flightCamera.Distance; + if (!spawning) + { + var overLand = (worldIndex != -1 ? FlightGlobals.Bodies[worldIndex] : FlightGlobals.currentMainBody).TerrainAltitude(latitude, longitude) > 0; + FlightGlobals.fetch.SetVesselPosition(worldIndex != -1 ? worldIndex : FlightGlobals.currentMainBody.flightGlobalsIndex, latitude, longitude, overLand ? Math.Max(5, altitude) : altitude, FlightGlobals.ActiveVessel.vesselType == VesselType.Plane ? 0 : 90, 0, true, overLand); // FIXME This should be using the vessel reference transform to determine the inclination. Also below. + FloatingOrigin.SetOffset(FlightGlobals.ActiveVessel.transform.position); // This adjusts local coordinates, such that the vessel position is (0,0,0). + VehiclePhysics.Gravity.Refresh(); + } + else + { + FlightGlobals.fetch.SetVesselPosition(worldIndex != -1 ? worldIndex : FlightGlobals.currentMainBody.flightGlobalsIndex, latitude, longitude, altitude, 0, 0, true); + var terrainAltitude = FlightGlobals.currentMainBody.TerrainAltitude(latitude, longitude); + var spawnPoint = FlightGlobals.currentMainBody.GetWorldSurfacePosition(latitude, longitude, terrainAltitude + altitude); + FloatingOrigin.SetOffset(spawnPoint); // This adjusts local coordinates, such that spawnPoint is (0,0,0). + var radialUnitVector = -FlightGlobals.currentMainBody.transform.position.normalized; + var cameraPosition = Vector3.RotateTowards(distance * radialUnitVector, Quaternion.AngleAxis(cameraHeading * Mathf.Rad2Deg, radialUnitVector) * -VectorUtils.GetNorthVector(spawnPoint, FlightGlobals.currentMainBody), 70f * Mathf.Deg2Rad, 0); + if (!spawnLocationCamera.activeSelf) + { + spawnLocationCamera.SetActive(true); + originalCameraParentTransform = flightCamera.transform.parent; + originalCameraNearClipPlane = GUIUtils.GetMainCamera().nearClipPlane; + } + spawnLocationCamera.transform.position = Vector3.zero; + spawnLocationCamera.transform.rotation = Quaternion.LookRotation(-cameraPosition, radialUnitVector); + flightCamera.transform.parent = spawnLocationCamera.transform; + flightCamera.SetTarget(spawnLocationCamera.transform); + flightCamera.transform.localPosition = cameraPosition; + flightCamera.transform.localRotation = Quaternion.identity; + flightCamera.ActivateUpdate(); + } + flightCamera.SetDistance(distance); + FlightCamera.CamHdg = cameraHeading; + FlightCamera.CamPitch = cameraPitch; + } + + IEnumerator DelayedShowSpawnPoint(int worldIndex, double latitude, double longitude, double altitude = 0, float distance = 0, bool spawning = false) + { + Vessel spawnProbe = VesselSpawner.SpawnSpawnProbe(); + if (spawnProbe != null) + { + spawnProbe.Landed = false; + yield return new WaitWhile(() => spawnProbe != null && (!spawnProbe.loaded || spawnProbe.packed)); + FlightGlobals.ForceSetActiveVessel(spawnProbe); + while (spawnProbe != null && FlightGlobals.ActiveVessel != spawnProbe) + { + spawnProbe.SetWorldVelocity(Vector3d.zero); + LoadedVesselSwitcher.Instance.ForceSwitchVessel(spawnProbe); + yield return waitForFixedUpdate; + } + } + ShowSpawnPoint(worldIndex, latitude, longitude, altitude, distance, spawning, false); + } + + public void RevertSpawnLocationCamera(bool keepTransformValues = true, bool revertIfDead = false) + { + if (spawnLocationCamera == null || !spawnLocationCamera.activeSelf) return; + if (BDArmorySettings.DEBUG_SPAWNING) Debug.Log($"[BDArmory.SpawnUtils]: Reverting spawn location camera."); + if (delayedShowSpawnPointCoroutine != null) { StopCoroutine(delayedShowSpawnPointCoroutine); delayedShowSpawnPointCoroutine = null; } + var flightCamera = FlightCamera.fetch; + if (originalCameraParentTransform != null) + { + if (keepTransformValues && flightCamera.transform != null && flightCamera.transform.parent != null) + { + originalCameraParentTransform.position = flightCamera.transform.parent.position; + originalCameraParentTransform.rotation = flightCamera.transform.parent.rotation; + originalCameraNearClipPlane = GUIUtils.GetMainCamera().nearClipPlane; + } + flightCamera.transform.parent = originalCameraParentTransform; + GUIUtils.GetMainCamera().nearClipPlane = originalCameraNearClipPlane; + flightCamera.SetTargetNone(); + flightCamera.EnableCamera(); + } + if (FlightGlobals.ActiveVessel != null && FlightGlobals.ActiveVessel.state != Vessel.State.DEAD) + LoadedVesselSwitcher.Instance.ForceSwitchVessel(FlightGlobals.ActiveVessel); // Update the camera. + else if (revertIfDead) // Spawn a spawn probe to avoid KSP breaking the camera. + { + var spawnProbe = VesselSpawner.SpawnSpawnProbe(flightCamera.Distance * flightCamera.mainCamera.transform.forward); + if (spawnProbe != null) + { + spawnProbe.Landed = false; + StartCoroutine(LoadedVesselSwitcher.Instance.SwitchToVesselWhenPossible(spawnProbe, 10)); + } + } + spawnLocationCamera.SetActive(false); + } + #endregion + + #region Intake hacks + public void HackIntakesOnNewVessels(bool enable) + { + if (enable) + { + GameEvents.onVesselLoaded.Add(HackIntakesEventHandler); + GameEvents.OnVesselRollout.Add(HackIntakes); + } + else + { + GameEvents.onVesselLoaded.Remove(HackIntakesEventHandler); + GameEvents.OnVesselRollout.Remove(HackIntakes); + } + } + void HackIntakesEventHandler(Vessel vessel) => HackIntakes(vessel, true); + + public void HackIntakes(Vessel vessel, bool enable) + { + if (vessel == null || !vessel.loaded) return; + if (enable) + { + foreach (var intake in VesselModuleRegistry.GetModules(vessel)) + intake.checkForOxygen = false; + } + else + { + foreach (var intake in VesselModuleRegistry.GetModules(vessel)) + { + var checkForOxygen = ConfigNodeUtils.FindPartModuleConfigNodeValue(intake.part.partInfo.partConfig, "ModuleResourceIntake", "checkForOxygen"); + if (!string.IsNullOrEmpty(checkForOxygen)) // Use the default value from the part. + { + try + { + intake.checkForOxygen = bool.Parse(checkForOxygen); + } + catch (Exception e) + { + Debug.LogError($"[BDArmory.BDArmorySetup]: Failed to parse checkForOxygen configNode of {intake.name}: {e.Message}"); + } + } + else + { + Debug.LogWarning($"[BDArmory.BDArmorySetup]: No default value for checkForOxygen found in partConfig for {intake.name}, defaulting to true."); + intake.checkForOxygen = true; + } + } + } + } + public void HackIntakes(ShipConstruct ship) // This version only needs to enable the hack. + { + if (ship == null) return; + foreach (var part in ship.Parts) + { + var intakes = part.FindModulesImplementing(); + if (intakes.Count() > 0) + { + foreach (var intake in intakes) + intake.checkForOxygen = false; + } + } + } + #endregion + #region Control Surface Actuator hacks + public void HackActuatorsOnNewVessels(bool enable) + { + if (enable) + { + GameEvents.onVesselLoaded.Add(HackActuatorsEventHandler); + GameEvents.OnVesselRollout.Add(HackActuators); + } + else + { + GameEvents.onVesselLoaded.Remove(HackActuatorsEventHandler); + GameEvents.OnVesselRollout.Remove(HackActuators); + } + } + void HackActuatorsEventHandler(Vessel vessel) => HackActuators(vessel, true); + + public void HackActuators(Vessel vessel, bool enable) + { + if (vessel == null || !vessel.loaded) return; + if (enable) + { + foreach (var ctrlSrf in VesselModuleRegistry.GetModules(vessel)) + { + ctrlSrf.actuatorSpeed = 30; + if (BDArmorySettings.DEBUG_SPAWNING) Debug.Log($"[BDArmory.ActuatorHacks]: Setting {ctrlSrf.name} actuation speed to : {ctrlSrf.actuatorSpeed}"); + } + } + else + { + foreach (var ctrlSrf in VesselModuleRegistry.GetModules(vessel)) + { + var actuatorSpeed = ConfigNodeUtils.FindPartModuleConfigNodeValue(ctrlSrf.part.partInfo.partConfig, "ModuleControlSurface", "actuatorSpeed"); + if (!string.IsNullOrEmpty(actuatorSpeed)) // Use the default value from the part. + { + try + { + ctrlSrf.actuatorSpeed = float.Parse(actuatorSpeed); + } + catch (Exception e) + { + Debug.LogError($"[BDArmory.BDArmorySetup]: Failed to parse actuatorSpeed configNode of {ctrlSrf.name}: {e.Message}"); + } + } + else + { + Debug.LogWarning($"[BDArmory.BDArmorySetup]: No default value for actuatorSpeed found in partConfig for {ctrlSrf.name}, defaulting to true."); + ctrlSrf.actuatorSpeed = 30; + } + } + } + } + public void HackActuators(ShipConstruct ship) // This version only needs to enable the hack. + { + if (ship == null) return; + foreach (var part in ship.Parts) + { + var ctrlSrf = part.FindModulesImplementing(); + if (ctrlSrf.Count() > 0) + { + foreach (var srf in ctrlSrf) + srf.actuatorSpeed = 30; + } + } + } + #endregion + #region Space hacks + public void SpaceFrictionOnNewVessels(bool enable) + { + if (enable) + { + GameEvents.onVesselLoaded.Add(SpaceHacksEventHandler); + GameEvents.OnVesselRollout.Add(SpaceHacks); + } + else + { + GameEvents.onVesselLoaded.Remove(SpaceHacksEventHandler); + GameEvents.OnVesselRollout.Remove(SpaceHacks); + } + } + void SpaceHacksEventHandler(Vessel vessel) => SpaceHacks(vessel); + + public void SpaceHacks(Vessel vessel) + { + if (vessel == null || !vessel.loaded) return; + + if (VesselModuleRegistry.GetMissileFire(vessel, true) != null && vessel.rootPart.FindModuleImplementing() == null) + { + vessel.rootPart.AddModule("ModuleSpaceFriction"); + } + } + public void SpaceHacks(ShipConstruct ship) // This version only needs to enable the hack. + { + if (ship == null) return; + ship.Parts[0].AddModule("ModuleSpaceFriction"); + } + #endregion + #region KAL + public void RestoreKAL(Vessel vessel, bool restore) => StartCoroutine(RestoreKALCoroutine(vessel, restore)); + /// + /// This goes through the vessel's part modules and fixes the mismatched part persistentId on the KAL's controlled axes with the correct ones in the ProtoPartModuleSnapshot then reloads the module from the ProtoPartModuleSnapshot. + /// + /// The vessel to modify. + /// Restore or wipe any KALs found. + IEnumerator RestoreKALCoroutine(Vessel vessel, bool restore) + { + var tic = Time.time; + yield return new Utils.WaitUntilFixed(() => vessel == null || vessel.Parts.Count != 0 || Time.time - tic > 10); // Wait for up to 10s for the vessel's parts to be populated (usually it takes 2 frames after spawning). + if (vessel == null || vessel.Parts.Count == 0) yield break; + if (!restore) // Wipe all KAL modules on the vessel. + { + foreach (var kal in vessel.FindPartModulesImplementing()) + { + if (kal == null) continue; + kal.ControlledAxes.Clear(); + } + yield break; + } + foreach (var protoPartSnapshot in vessel.protoVessel.protoPartSnapshots) // The protoVessel contains the original ProtoPartModuleSnapshots with the info we need. + foreach (var protoPartModuleSnapshot in protoPartSnapshot.modules) + if (protoPartModuleSnapshot.moduleName == "ModuleRoboticController") // Found a KAL + { + var kal = protoPartModuleSnapshot.moduleRef as Expansions.Serenity.ModuleRoboticController; + var controlledAxes = protoPartModuleSnapshot.moduleValues.GetNode("CONTROLLEDAXES"); + kal.ControlledAxes.Clear(); // Clear the existing axes (they should be clear already due to mismatching part persistent IDs, but better safe than sorry). + int rowIndex = 0; + foreach (var axisNode in controlledAxes.GetNodes("AXIS")) // For each axis to be controlled, locate the part in the spawned vessel that has the correct module. + if (uint.TryParse(axisNode.GetValue("moduleId"), out uint moduleId)) // Get the persistentId of the module it's supposed to be affecting, which is correctly set in some part. + { + foreach (var part in vessel.Parts) + foreach (var partModule in part.Modules) + if (partModule.PersistentId == moduleId) // Found a corresponding part with the correct moduleId. Note: there could be multiple parts with this module due to symmetry, so we check them all. + { + var fieldName = axisNode.GetValue("axisName"); + foreach (var field in partModule.Fields) + if (field.name == fieldName) // Found the axis field in a module in a part being controlled by this KAL. + { + axisNode.SetValue("persistentId", part.persistentId.ToString()); // Update the ConfigNode in the ProtoPartModuleSnapshot + axisNode.SetValue("partNickName", part.partInfo.title); // Set the nickname to the part title (note: this will override custom nicknames). + axisNode.SetValue("rowIndex", rowIndex++); + var axis = new Expansions.Serenity.ControlledAxis(part, partModule, field as BaseAxisField, kal); // Link the part, module, field and KAL together. + axis.Load(axisNode); // Load the new config into the axis. + kal.ControlledAxes.Add(axis); // Add the axis to the KAL. + break; + } + } + } + } + } + #endregion + } +} \ No newline at end of file diff --git a/BDArmory/VesselSpawning/VesselMover.cs b/BDArmory/VesselSpawning/VesselMover.cs new file mode 100644 index 000000000..ad9931c98 --- /dev/null +++ b/BDArmory/VesselSpawning/VesselMover.cs @@ -0,0 +1,1434 @@ +using UnityEngine; +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using KSP.UI.Screens; + +using BDArmory.Extensions; +using BDArmory.Settings; +using BDArmory.UI; +using BDArmory.Utils; + +using static BDArmory.VesselSpawning.CustomTemplateSpawning; // For the CustomCraftBrowserDialog + +namespace BDArmory.VesselSpawning +{ + [KSPAddon(KSPAddon.Startup.Flight, false)] + public class VesselMover : VesselSpawnerBase + { + public static VesselMover Instance; + public static ApplicationLauncherButton button; + public static bool buttonSetup; + + enum State { None, Moving, Lowering, Spawning }; + State state + { + get { return _state; } + set { _state = value; ResetWindowHeight(); } + } + State _state; + internal WaitForFixedUpdate wait = new WaitForFixedUpdate(); + HashSet movingVessels = new HashSet(); + HashSet loweringVessels = new HashSet(); + List jumpToAltitudes = new List { 10, 100, 1000, 10000, 50000 }; + RaycastHit[] hits = new RaycastHit[10]; + + #region Monobehaviour routines + protected override void Awake() + { + base.Awake(); + if (Instance != null) Destroy(Instance); + Instance = this; + } + + void Start() + { + ready = false; + StartCoroutine(WaitForBdaSettings()); + ConfigureStyles(); + moveIndicator = new GameObject().AddComponent(); + moveIndicator.material = new Material(Shader.Find("KSP/Emissive/Diffuse")); + moveIndicator.material.SetColor("_EmissiveColor", Color.green); + moveIndicator.startWidth = 0.15f; + moveIndicator.endWidth = 0.15f; + moveIndicator.enabled = false; + moveIndicator.positionCount = circleRes + 3; + GameEvents.onVesselChange.Add(OnVesselChanged); + + if (BDArmorySettings.VM_TOOLBAR_BUTTON) AddToolbarButton(); + } + + private IEnumerator WaitForBdaSettings() + { + yield return new WaitUntil(() => BDArmorySettings.ready); + + BDArmorySetup.WindowRectVesselMover.height = 0; + if (guiCheckIndex < 0) guiCheckIndex = GUIUtils.RegisterGUIRect(new Rect()); + if (_vesselGUICheckIndex < 0) _vesselGUICheckIndex = GUIUtils.RegisterGUIRect(new Rect()); + if (_crewGUICheckIndex < 0) _crewGUICheckIndex = GUIUtils.RegisterGUIRect(new Rect()); + ready = true; + BDArmorySetup.Instance.hasVesselMover = true; + SetVisible(BDArmorySetup.showVesselMoverGUI); + } + + void OnDestroy() + { + GameEvents.onVesselChange.Remove(OnVesselChanged); + } + + void Update() + { + if (state != State.None && FlightGlobals.ActiveVessel == null) state = State.None; + if (state == State.Moving && IsMoving(FlightGlobals.ActiveVessel)) + HandleInput(); // Input needs to be handled in Update. + } + + void LateUpdate() + { + if (state == State.Moving && !MapView.MapIsEnabled && BDArmorySetup.GAME_UI_ENABLED) + { + moveIndicator.enabled = true; + DrawMovingIndicator(); + } + else moveIndicator.enabled = false; + } + #endregion + + #region Input + Vector3 positionAdjustment = Vector3.zero; // X, Y, Z + Vector3 rotationAdjustment = Vector3.zero; // Roll, Yaw, Pitch + bool translating = false; + bool rotating = false; + bool jump = false; + bool jumpDirection = false; // false = down, true = up + bool reset = false; + bool autoLevelPlane = false; + bool autoLevelRocket = false; + void HandleInput() + { + positionAdjustment = Vector3.zero; + rotationAdjustment = Vector3.zero; + translating = false; + rotating = false; + autoLevelPlane = false; + autoLevelRocket = false; + + if (!MapView.MapIsEnabled) // No altitude changes while in map mode. + { + if (GameSettings.THROTTLE_CUTOFF.GetKeyDown()) // Reset altitude to base. + { reset = true; } + else if (Input.GetKeyDown(KeyCode.Tab)) // Jump to next reference altitude. + { + jump = true; + jumpDirection = GameSettings.THROTTLE_UP.GetKey(); + } + else if (GameSettings.THROTTLE_UP.GetKey()) // Increase altitude. + { + positionAdjustment.z = 1f; + translating = true; + } + else if (GameSettings.THROTTLE_DOWN.GetKey()) // Decrease altitude. + { + positionAdjustment.z = -1f; + translating = true; + } + } + + if (GameSettings.PITCH_DOWN.GetKey()) // Translate forward. + { + positionAdjustment.y = 1f; + translating = true; + } + else if (GameSettings.PITCH_UP.GetKey()) // Translate backward. + { + positionAdjustment.y = -1f; + translating = true; + } + + if (GameSettings.YAW_RIGHT.GetKey()) // Translate right. + { + positionAdjustment.x = 1f; + translating = true; + } + else if (GameSettings.YAW_LEFT.GetKey()) // Translate left. + { + positionAdjustment.x = -1f; + translating = true; + } + + if (GameSettings.TRANSLATE_FWD.GetKey()) // Auto-level plane + { autoLevelPlane = true; rotating = true; } + else if (GameSettings.TRANSLATE_BACK.GetKey()) // Auto-level rocket + { autoLevelRocket = true; rotating = true; } + else + { + if (GameSettings.ROLL_LEFT.GetKey()) // Roll left. + { + rotationAdjustment.x = -1f; + rotating = true; + } + else if (GameSettings.ROLL_RIGHT.GetKey()) // Roll right. + { + rotationAdjustment.x = 1f; + rotating = true; + } + + if (GameSettings.TRANSLATE_DOWN.GetKey()) // Pitch down. + { + rotationAdjustment.z = 1f; + rotating = true; + } + else if (GameSettings.TRANSLATE_UP.GetKey()) // Pitch up. + { + rotationAdjustment.z = -1f; + rotating = true; + } + + if (GameSettings.TRANSLATE_RIGHT.GetKey()) // Yaw right. + { + rotationAdjustment.y = 1f; + rotating = true; + } + else if (GameSettings.TRANSLATE_LEFT.GetKey()) // Yaw left. + { + rotationAdjustment.y = -1f; + rotating = true; + } + } + } + #endregion + + #region Moving + Vector3d geoCoords; + public bool IsValid(Vessel vessel) => vessel != null && vessel.loaded && !vessel.packed; + public bool IsMoving(Vessel vessel) => IsValid(vessel) && movingVessels.Contains(vessel); + public bool IsLowering(Vessel vessel) => IsValid(vessel) && loweringVessels.Contains(vessel); + IEnumerator MoveVessel(Vessel vessel) + { + if (!IsValid(vessel)) { state = State.None; yield break; } + if (BDArmorySettings.DEBUG_SPAWNING) Debug.Log($"[BDArmory.VesselMover]: Moving {vessel.vesselName}"); + movingVessels.Add(vessel); + state = State.Moving; + + var hadPatchedConicsSolver = vessel.PatchedConicsAttached; + if (hadPatchedConicsSolver) + { + if (BDArmorySettings.DEBUG_SPAWNING) Debug.Log($"[BDArmory.VesselMover]: Detaching patched conic solver"); + try + { + vessel.DetachPatchedConicsSolver(); + } + catch (Exception e) + { + Debug.LogWarning($"[BDArmory.VesselMover]: Failed to remove the Patched Conic Solver: {e.Message}"); + } + } + + // Disable various action groups. We'll enable some of them again later if specified. + vessel.ActionGroups.SetGroup(KSPActionGroup.RCS, false); // Disable RCS + if (BDArmorySettings.VESSEL_MOVER_ENABLE_SAS) vessel.ActionGroups.SetGroup(KSPActionGroup.SAS, false); // Disable SAS + if (BDArmorySettings.VESSEL_MOVER_ENABLE_BRAKES) vessel.ActionGroups.SetGroup(KSPActionGroup.Brakes, false); // Disable Brakes + foreach (LaunchClamp clamp in vessel.FindPartModulesImplementing()) clamp.Release(); // Release clamps + KillRotation(vessel); + if (BDArmorySettings.VESSEL_MOVER_ENABLE_BRAKES) vessel.ActionGroups.SetGroup(KSPActionGroup.Brakes, true); + + var initialUp = (vessel.transform.position - FlightGlobals.currentMainBody.transform.position).normalized; + var up = initialUp; + Vector3 forward = default, right = default; + float startingAltitude = 2f * vessel.GetRadius(); + var lowerBound = GetLowerBound(vessel); + var safeAlt = SafeAltitude(vessel, lowerBound); + var position = vessel.transform.position; + var rotation = vessel.transform.rotation; + var referenceTransform = vessel.ReferenceTransform; + if (LandedOrSplashed(vessel) || startingAltitude > safeAlt) // Careful with initial separation from ground. + { + var count = 0; + while (IsMoving(vessel) && ++count <= 10) + { + vessel.Landed = false; + vessel.Splashed = false; + vessel.IgnoreGForces(240); + vessel.IgnoreSpeed(240); + position += count * (startingAltitude - safeAlt) / 55f * up; + vessel.SetPosition(position); + vessel.SetWorldVelocity(Vector3d.zero); + vessel.SetRotation(rotation); + yield return wait; + KrakensbaneCorrection(ref position); + } + } + + KillRotation(vessel); + float moveSpeed = 0; + float rotateSpeed = 0; + var coordinateFrameAdjustAngle = Mathf.Cos(Mathf.Deg2Rad * 0.1f); + float preMapViewAltitude = 0; + while (IsMoving(vessel)) + { + if (vessel.isActiveVessel) + { + if (translating || autoLevelPlane || autoLevelRocket) + { + up = (vessel.transform.position - FlightGlobals.currentMainBody.transform.position).normalized; + if (Vector3.Dot(initialUp, up) < coordinateFrameAdjustAngle) // Up changed by > coordinateFrameAdjustAngle: rotate the vessel and reset initialUp. + { + rotation = Quaternion.FromToRotation(initialUp, up) * rotation; + rotating = true; + initialUp = up; + } + if (MapView.MapIsEnabled) + { + forward = -Math.Sign(vessel.latitude) * (vessel.mainBody.GetWorldSurfacePosition(vessel.latitude - Math.Sign(vessel.latitude), vessel.longitude, vessel.altitude) - vessel.GetWorldPos3D()).ProjectOnPlanePreNormalized(up).normalized; + } + else + { + forward = (vessel.transform.position - FlightCamera.fetch.mainCamera.transform.position).ProjectOnPlanePreNormalized(up).normalized; + } + right = Vector3.Cross(up, forward); + } + + // Perform rotations first to update lower bound. + if (rotating) + { + var radarAltitude = RadarAltitude(vessel) - lowerBound; + rotateSpeed = Mathf.Clamp(Mathf.MoveTowards(rotateSpeed, 180f, Mathf.Min(10f + 10f * rotateSpeed, 180f) * Time.fixedDeltaTime), 0f, 180f); + } + else { rotateSpeed = 0f; } + if (autoLevelPlane) + { + Quaternion targetRot = Quaternion.LookRotation(-up, forward); + var angleDelta = Quaternion.Angle(rotation, targetRot); + if (angleDelta > 0) // Note Quaternion.Angle is considered 0 below around 0.03, so this only runs a few times when finally approaching the target angle. + { + if (angleDelta < rotateSpeed * 4f * Time.fixedDeltaTime) rotation = Quaternion.Slerp(rotation, targetRot, 0.5f); // Slerp the last part to avoid overshooting, which shouldn't happen, but does! + else rotation = Quaternion.RotateTowards(rotation, targetRot, rotateSpeed * 2f * Time.fixedDeltaTime); + } + } + else if (autoLevelRocket) + { + Quaternion targetRot = Quaternion.LookRotation(forward, up); + rotation = Quaternion.RotateTowards(rotation, targetRot, rotateSpeed * 2f * Time.fixedDeltaTime); + } + else if (rotating) + { + if (rotationAdjustment.x != 0) rotation = Quaternion.AngleAxis(-rotateSpeed * Time.fixedDeltaTime * rotationAdjustment.x, referenceTransform.up) * rotation; // Roll + if (rotationAdjustment.z != 0) rotation = Quaternion.AngleAxis(rotateSpeed * Time.fixedDeltaTime * rotationAdjustment.z, referenceTransform.right) * rotation; // Pitch + if (rotationAdjustment.y != 0) rotation = Quaternion.AngleAxis(-rotateSpeed * Time.fixedDeltaTime * rotationAdjustment.y, referenceTransform.forward) * rotation; // Yaw + } + if (rotating) + { + vessel.IgnoreGForces(240); + vessel.IgnoreSpeed(240); + var previousLowerBound = lowerBound; + vessel.SetRotation(rotation); + lowerBound = GetLowerBound(vessel); + vessel.SetPosition(position); + vessel.SetWorldVelocity(Vector3d.zero); + position += (lowerBound - previousLowerBound) * up; + } + + // Translations/Altitude changes + if (MapView.MapIsEnabled && preMapViewAltitude == 0) // Map View was enabled, raise the craft to at least 10km. + { + preMapViewAltitude = RadarAltitude(vessel); // When map view gets enabled, store the altitude the craft was at. + var altitude = Mathf.Max(Mathf.Max((float)vessel.mainBody.Radius * 0.05f, 1e4f), preMapViewAltitude); // Then raise the vessel to at least 10km to avoid terrain loading. + var safeAltitude = SafeAltitude(vessel, lowerBound); + position += (altitude - safeAltitude) * up; + if (BDArmorySettings.DEBUG_SPAWNING) Debug.Log($"[BDArmory.VesselMover]: Switching to map altitude {altitude + lowerBound}m"); + } + else if (!MapView.MapIsEnabled && preMapViewAltitude != 0) // Map View was disabled, lower the craft back to it's original altitude (min base+1km). Leave horizontal movement for later frames. + { + preMapViewAltitude = Mathf.Max(preMapViewAltitude, 2f * vessel.GetRadius() + 1000f); + var safeAltitude = SafeAltitude(vessel, lowerBound); + position += (preMapViewAltitude - safeAltitude) * up; + if (BDArmorySettings.DEBUG_SPAWNING) Debug.Log($"[BDArmory.VesselMover]: Reverting to pre-map altitude {preMapViewAltitude + lowerBound}m (safeAlt: {safeAltitude}, lower bound: {lowerBound}m)"); + preMapViewAltitude = 0; + } + else if (reset) + { + var baseAltitude = 2f * vessel.GetRadius(); + var safeAltitude = SafeAltitude(vessel, lowerBound); + if (!BDArmorySettings.VESSEL_MOVER_BELOW_WATER) safeAltitude = Mathf.Min((float)vessel.altitude, safeAltitude); + if (BDArmorySettings.DEBUG_SPAWNING) Debug.Log($"[BDArmory.VesselMover]: Resetting to base altitude {baseAltitude + lowerBound}m (safeAlt: {safeAltitude}, lower bound: {lowerBound}m)"); + position += (baseAltitude - safeAltitude) * up; + reset = false; + } + else if (jump) + { + var baseAltitude = 2f * vessel.GetRadius(); + var safeAltitude = SafeAltitude(vessel, lowerBound); + var jumpToAltitude = baseAltitude; + if (jumpDirection) // Jump up + { + jumpToAltitude = jumpToAltitudes.Where(a => a > 1.05f * safeAltitude && a > 2f * baseAltitude).Select(a => Mathf.Max(a, baseAltitude)).FirstOrDefault(); + if (jumpToAltitude < baseAltitude) jumpToAltitude = baseAltitude; + } + else // Jump down + { + jumpToAltitude = safeAltitude < 1.1f * baseAltitude ? jumpToAltitudes.Last() : jumpToAltitudes.Where(a => a < 0.95f * safeAltitude).LastOrDefault(); + if (jumpToAltitude < 2f * baseAltitude) jumpToAltitude = baseAltitude; + } + if (BDArmorySettings.DEBUG_SPAWNING) Debug.Log($"[BDArmory.VesselMover]: Jumping to altitude {jumpToAltitude}m (safeAlt: {safeAltitude}, baseAlt: {baseAltitude})"); + up = (vessel.transform.position - FlightGlobals.currentMainBody.transform.position).normalized; + position += (jumpToAltitude - safeAltitude) * up; + jump = false; + } + else if (translating) + { + var radarAltitude = RadarAltitude(vessel); + var distance = Mathf.Abs(radarAltitude - lowerBound); + float maxMoveSpeed = MapView.MapIsEnabled ? 1e5f : (distance < 1e4f ? 10f + distance : 100f * BDAMath.Sqrt(distance)); + moveSpeed = Mathf.Clamp(Mathf.MoveTowards(moveSpeed, maxMoveSpeed, (4.79f * moveSpeed + 0.05f * maxMoveSpeed) * Time.fixedDeltaTime), 0, maxMoveSpeed); // Accelerated acceleration for ~2s. + + var moveDistanceHorizontal = 10f * moveSpeed * Time.fixedDeltaTime; + var moveDistanceVertical = moveSpeed * Time.fixedDeltaTime; + var offset = positionAdjustment.x * moveDistanceHorizontal * right + positionAdjustment.y * moveDistanceHorizontal * forward; + offset += (radarAltitude - RadarAltitude(position + offset)) * up; + var safeAltitude = radarAltitude < 1000 ? SafeAltitude(vessel, lowerBound, offset) : radarAltitude; // Don't bother when over 1000m. + offset += Mathf.Max(positionAdjustment.z * moveDistanceVertical, -safeAltitude) * up; + position += offset; + // Debug.Log($"DEBUG position: {position:G6}, altitude: {radarAltitude}, safeAltCorrection: {safeAltitude}, distance: {distance}, moveSpeed: {moveSpeed}, maxSpeed: {maxMoveSpeed}, moveDistanceHorizontal: {moveDistanceHorizontal}, moveDistanceVertical: {moveDistanceVertical}"); + } + else { moveSpeed = 0; } + } + + vessel.IgnoreGForces(240); + vessel.IgnoreSpeed(240); + vessel.SetPosition(position); + vessel.SetWorldVelocity(Vector3d.zero); + vessel.SetRotation(rotation); // Reset the rotation to prevent any angular momentum from messing with the orientation. + yield return wait; + KrakensbaneCorrection(ref position); + } + + if (hadPatchedConicsSolver && !vessel.PatchedConicsAttached) + { + if (BDArmorySettings.DEBUG_SPAWNING) Debug.Log($"[BDArmory.VesselMover]: Re-attaching patched conic solver"); + try + { + vessel.AttachPatchedConicsSolver(); + if (vessel.altitude > 1e5) vessel.SetWorldVelocity(UnityEngine.Random.rotation * Vector3.one * 0.01f); // Add noise to the velocity if above 100km to avoid NaNs in the Patched Cubic Solver due to a degenerate orbit. + } + catch (Exception e) + { + Debug.LogWarning($"[BDArmory.VesselMover]: Failed to re-attach the Patched Conic Solver: {e.Message}"); + } + } + } + + void KillRotation(Vessel vessel) + { + if (vessel.angularVelocity == default) return; + foreach (var part in vessel.Parts) + { + var rb = part.Rigidbody; + if (rb == null) continue; + rb.angularVelocity = default; + } + } + + void KrakensbaneCorrection(ref Vector3 position) + { + if (!BDKrakensbane.IsActive) return; + position -= BDKrakensbane.FloatingOriginOffsetNonKrakensbane; + } + + public IEnumerator PlaceVessel(Vessel vessel, bool skipMovingCheck = false) + { + if (IsLowering(vessel)) yield break; // We're already doing this. + if (!skipMovingCheck && !IsMoving(vessel)) { state = State.None; yield break; } // The vessel isn't moving, abort. + if (BDArmorySettings.DEBUG_SPAWNING) Debug.Log($"[BDArmory.VesselMover]: Placing {vessel.vesselName}"); + movingVessels.Remove(vessel); + loweringVessels.Add(vessel); + + KillRotation(vessel); + if (BDArmorySettings.VESSEL_MOVER_ENABLE_BRAKES) vessel.ActionGroups.SetGroup(KSPActionGroup.Brakes, true); + if (!FlightGlobals.currentMainBody.hasSolidSurface) { DropVessel(vessel); yield break; } // No surface to lower to! + if (BDArmorySettings.VESSEL_MOVER_LOWER_FAST) + { + var up = (vessel.transform.position - FlightGlobals.currentMainBody.transform.position).normalized; + var baseAltitude = 2f * vessel.GetRadius(); + var safeAltitude = SafeAltitude(vessel) * 0.95f; // Only go 95% of the way in a single jump in case terrain is still loading in. + if (baseAltitude < safeAltitude) + vessel.Translate((baseAltitude - safeAltitude) * up); + vessel.SetWorldVelocity(Vector3d.zero); + } + yield return LowerVessel(vessel); + } + + IEnumerator LowerVessel(Vessel vessel) + { + if (!FlightGlobals.currentMainBody.hasSolidSurface) { DropVessel(vessel); yield break; } // No surface to lower to! + if (BDArmorySettings.DEBUG_SPAWNING) Debug.Log($"[BDArmory.VesselMover]: Lowering {vessel.vesselName}"); + state = State.Lowering; + var lowerBound = GetLowerBound(vessel); + var up = (vessel.transform.position - FlightGlobals.currentMainBody.transform.position).normalized; + bool finalLowering = false; + var previousAltitude = vessel.altitude; + while (IsLowering(vessel) && !LandedOrSplashed(vessel) && (!finalLowering || vessel.altitude - previousAltitude < -0.1 * BDArmorySettings.VESSEL_MOVER_MIN_LOWER_SPEED * Time.fixedDeltaTime)) + { + var distance = SafeAltitude(vessel, lowerBound); + var speed = distance > 1e4 ? 100f * BDAMath.Sqrt(distance) : distance > BDArmorySettings.VESSEL_MOVER_MIN_LOWER_SPEED ? Mathf.Clamp(1f + 30f / distance, 1f, 4f) * distance : BDArmorySettings.VESSEL_MOVER_MIN_LOWER_SPEED; + if (speed > BDArmorySettings.VESSEL_MOVER_MIN_LOWER_SPEED) + { + vessel.SetWorldVelocity(Vector3d.zero); + vessel.Translate(-speed * up * Time.fixedDeltaTime); + } + else + { + if (!finalLowering && vessel.verticalSpeed < -1e-2) finalLowering = true; + vessel.SetWorldVelocity(-speed * up); + } + // Debug.Log($"DEBUG landed/splashed: {LandedOrSplashed(vessel)}, altitude: {vessel.altitude}m ({distance}m), v-speed: {vessel.verticalSpeed}m/s, speed: {speed}"); + previousAltitude = vessel.altitude; + yield return wait; + } + if (!IsLowering(vessel)) yield break; // Vessel destroyed or state switched, e.g., moving again. + + // Turn on brakes and SAS (apparently helps to avoid the turning bug). + if (BDArmorySettings.VESSEL_MOVER_ENABLE_SAS) + { vessel.ActionGroups.SetGroup(KSPActionGroup.SAS, true); } + if (BDArmorySettings.VESSEL_MOVER_ENABLE_BRAKES) + { + vessel.ActionGroups.SetGroup(KSPActionGroup.Brakes, true); + // Ease the craft to a resting position. + messageState = Messages.EasingCraft; + var startTime = Time.time; + var stationaryStartTime = startTime; + vessel.IgnoreGForces(0); + vessel.IgnoreSpeed(0); + while (IsLowering(vessel) && Time.time - startTime < 10f && Time.time - stationaryStartTime <= 0.1f) // Damp movement for up to 10s. + { + // if ((float)vessel.verticalSpeed < -0.1f * BDArmorySettings.VESSEL_MOVER_MIN_LOWER_SPEED) + if ((vessel.altitude >= 0 && Math.Abs(vessel.altitude - previousAltitude) > 0.1 * BDArmorySettings.VESSEL_MOVER_MIN_LOWER_SPEED * Time.fixedDeltaTime) + || (vessel.altitude < 0 && vessel.altitude - previousAltitude < -0.1 * BDArmorySettings.VESSEL_MOVER_MIN_LOWER_SPEED * Time.fixedDeltaTime)) + { + vessel.SetWorldVelocity(vessel.GetSrfVelocity() * (0.45f + 0.5f * BDArmorySettings.VESSEL_MOVER_MIN_LOWER_SPEED)); + stationaryStartTime = Time.time; + yield return wait; // Setting the velocity prevents a proper velocity calculation on the next frame, so wait an extra frame for it to take effect. + } + // Debug.Log($"DEBUG landed/splashed: {LandedOrSplashed(vessel)}, altitude: {vessel.altitude}m, v-speed: {vessel.verticalSpeed}m/s"); + previousAltitude = vessel.altitude; + yield return wait; + } + } + if (IsLowering(vessel)) + { + loweringVessels.Remove(vessel); + state = State.None; + messageState = Messages.None; + } + } + + void DropVessel(Vessel vessel) + { + if (!IsMoving(vessel) && !IsLowering(vessel)) return; // Not in a valid state for dropping. + if (BDArmorySettings.DEBUG_SPAWNING) Debug.Log($"[BDArmory.VesselMover]: Dropping {vessel.vesselName}"); + movingVessels.Remove(vessel); + loweringVessels.Remove(vessel); + state = State.None; + messageState = Messages.None; + } + + /// + /// Get the vertical distance (non-negative) from the vessel transform position to the lowest point. + /// + /// + /// + float GetLowerBound(Vessel vessel) + { + var up = (vessel.transform.position - FlightGlobals.currentMainBody.transform.position).normalized; + var maxDim = 2f * vessel.GetRadius(); + var radius = vessel.GetRadius(up, vessel.GetBounds()); + var hitCount = Physics.BoxCastNonAlloc(vessel.transform.position - (maxDim + 0.1f) * up, new Vector3(radius, 0.1f, radius), up, hits, Quaternion.FromToRotation(Vector3.up, up), maxDim, (int)(LayerMasks.Parts | LayerMasks.EVA | LayerMasks.Wheels)); + if (hitCount == hits.Length) + { + hits = Physics.BoxCastAll(vessel.transform.position - (maxDim + 0.1f) * up, new Vector3(radius, 0.1f, radius), up, Quaternion.FromToRotation(Vector3.up, up), maxDim, (int)(LayerMasks.Parts | LayerMasks.EVA | LayerMasks.Wheels)); + hitCount = hits.Length; + } + var distances = hits.Take(hitCount).Where(hit => hit.collider != null && hit.collider.gameObject != null).Where(hit => { var part = hit.collider.gameObject.GetComponentInParent(); return part != null && part.vessel == vessel; }).Select(hit => hit.distance).ToArray(); + if (distances.Length == 0) + { + Debug.LogWarning($"[BDArmory.VesselMover]: Failed to detect craft for lower bound!"); + return 0; + } + return maxDim - distances.Min(); + } + + bool LandedOrSplashed(Vessel vessel) => BDArmorySettings.VESSEL_MOVER_BELOW_WATER ? vessel.Landed : vessel.LandedOrSplashed; + float RadarAltitude(Vessel vessel) => (float)(vessel.altitude - vessel.mainBody.TerrainAltitude(vessel.latitude, vessel.longitude, BDArmorySettings.VESSEL_MOVER_BELOW_WATER)); + float RadarAltitude(Vector3 position) => (float)(FlightGlobals.currentMainBody.GetAltitude(position) - BodyUtils.GetTerrainAltitudeAtPos(position, BDArmorySettings.VESSEL_MOVER_BELOW_WATER)); + float SafeAltitude(Vessel vessel, float lowerBound = -1f, Vector3 offset = default) // Get the safe altitude range we can adjust by. + { + var altitude = RadarAltitude(vessel); + if (BDArmorySettings.VESSEL_MOVER_DONT_WORRY_ABOUT_COLLISIONS && state == State.Moving) return altitude; + var position = vessel.transform.position + offset; + var up = (position - FlightGlobals.currentMainBody.transform.position).normalized; + var radius = vessel.GetRadius(up, vessel.GetBounds()); + if (lowerBound < 0) lowerBound = GetLowerBound(vessel); + + // Detect collisions from moving in the direction of the offset. 100m is generally sufficient. + var hitCount = Physics.BoxCastNonAlloc(position + 100.1f * up, new Vector3(radius, 0.1f, radius), -up, hits, Quaternion.FromToRotation(Vector3.up, up), altitude + 100f, (int)(LayerMasks.Scenery | LayerMasks.Parts | LayerMasks.EVA | LayerMasks.Wheels)); + if (hitCount == hits.Length) + { + hits = Physics.BoxCastAll(position + 100.1f * up, new Vector3(radius, 0.1f, radius), -up, Quaternion.FromToRotation(Vector3.up, up), altitude + 100f, (int)(LayerMasks.Scenery | LayerMasks.Parts | LayerMasks.EVA | LayerMasks.Wheels)); + hitCount = hits.Length; + } + if (hitCount > 0) + { + var distances = hits.Take(hitCount).Where(hit => hit.collider != null && hit.collider.gameObject != null).Where(hit => { var part = hit.collider.gameObject.GetComponentInParent(); return part == null || part.vessel != vessel; }).Select(hit => hit.distance).ToArray(); + if (distances.Length > 0) altitude = Mathf.Min(altitude, distances.Min() - 100f); + } + return altitude - lowerBound - 0.1f; + } + #endregion + + #region Spawning + Vessel spawnedVessel; + HashSet KerbalNames = new HashSet(); + int crewCapacity = -1; + string vesselNameToSpawn = ""; + CustomCraftBrowserDialog craftBrowser; + bool abortCraftSelection = false; + IEnumerator SpawnVessel() + { + state = State.Spawning; + + // Open craft selection + string craftFile = ""; + abortCraftSelection = false; + messageState = Messages.OpeningCraftBrowser; + if (BDArmorySettings.VESSEL_MOVER_CLASSIC_CRAFT_CHOOSER) + { + var craftBrowser = CraftBrowserDialog.Spawn(EditorFacility.SPH, HighLogic.SaveFolder, (path, loadType) => { craftFile = path; }, () => { abortCraftSelection = true; }, false); + while (!abortCraftSelection && string.IsNullOrEmpty(craftFile)) yield return wait; + if (craftBrowser != null) craftBrowser.Dismiss(); + craftBrowser = null; + } + else + { + ShowVesselSelection((path) => { craftFile = path; }, () => { abortCraftSelection = true; }); + while (!abortCraftSelection && string.IsNullOrEmpty(craftFile)) yield return wait; + } + messageState = Messages.None; + if (abortCraftSelection || string.IsNullOrEmpty(craftFile)) { state = State.None; yield break; } + if (BDArmorySettings.DEBUG_SPAWNING) Debug.Log($"[BDArmory.VesselMover]: {craftFile} selected for spawning."); + + // Choose crew + crewCapacity = GetCrewCapacity(craftFile, out vesselNameToSpawn); + if (BDArmorySettings.VESSEL_MOVER_CHOOSE_CREW) + { yield return ChooseCrew(); } + messageState = Messages.None; + + // Select location + yield return GetSpawnPoint(); + messageState = Messages.None; + if (geoCoords == Vector3d.zero) { state = State.None; yield break; } + + // Store the camera view + var camera = FlightCamera.fetch; + var cameraOffset = FlightGlobals.ActiveVessel != null ? camera.transform.position - FlightGlobals.ActiveVessel.transform.position : Vector3.zero; + + // Spawn the craft + yield return SpawnVessel(craftFile, geoCoords.x, geoCoords.y, geoCoords.z + 1000f, kerbalNames: KerbalNames); // Spawn 1km higher than requested and then move it down. + messageState = Messages.None; + if (spawnFailureReason != SpawnFailureReason.None) { state = State.None; yield break; } + if (BDArmorySettings.DEBUG_SPAWNING) Debug.Log($"[BDArmory.VesselMover]: Spawned {spawnedVessel.vesselName} at {geoCoords:G6}"); + while (spawnedVessel != null && (!spawnedVessel.loaded || spawnedVessel.packed)) yield return wait; + if (spawnedVessel != null) + { + var up = (spawnedVessel.transform.position - FlightGlobals.currentMainBody.transform.position).normalized; + if (FlightGlobals.currentMainBody.hasSolidSurface) + { + var safeAltitude = SafeAltitude(spawnedVessel); + if (!BDArmorySettings.VESSEL_MOVER_BELOW_WATER) safeAltitude = Mathf.Min((float)spawnedVessel.altitude, safeAltitude); + spawnedVessel.Translate((2f * spawnedVessel.GetRadius() - safeAltitude) * up); + } + else // No surface to lower to! + { + spawnedVessel.Translate(-1000f * up); + } + spawnedVessel.SetWorldVelocity(Vector3d.zero); + } + + // Switch to it when possible + yield return LoadedVesselSwitcher.Instance.SwitchToVesselWhenPossible(spawnedVessel); + if (!IsValid(spawnedVessel)) + { + Debug.LogWarning($"[BDArmory.VesselMover]: The spawned vessel disappeared before we could switch to it!"); + state = State.None; + yield break; + } + + // Restore the camera view + camera.SetCamCoordsFromPosition(spawnedVessel.transform.position + cameraOffset); + + // Switch to moving mode + if (BDArmorySettings.VESSEL_MOVER_PLACE_AFTER_SPAWN) + { + yield return PlaceVessel(spawnedVessel, true); + } + else + { + yield return MoveVessel(spawnedVessel); + } + spawnedVessel = null; // Clear the reference to the spawned vessel. + } + + void RecoverVessel() + { + var vessel = FlightGlobals.ActiveVessel; + var nearestOtherVessel = FlightGlobals.VesselsLoaded.Where(v => v != vessel).OrderBy(v => (v.transform.position - vessel.transform.position).sqrMagnitude).FirstOrDefault(); + if (nearestOtherVessel != null) + { + if (BDArmorySettings.DEBUG_SPAWNING) Debug.Log($"[BDArmory.VesselMover]: Switching to nearest vessel {nearestOtherVessel.vesselName}"); + LoadedVesselSwitcher.Instance.ForceSwitchVessel(nearestOtherVessel); + } + SpawnUtils.RemoveVessel(vessel); + } + + int GetCrewCapacity(string craftFile, out string vesselName) + { + CraftProfileInfo.PrepareCraftMetaFileLoad(); + var craftMeta = $"{Path.GetFileNameWithoutExtension(craftFile)}.loadmeta"; + var meta = new CraftProfileInfo(); + if (File.Exists(craftMeta)) // If the loadMeta file exists, use it, otherwise generate one. + { + meta.LoadFromMetaFile(craftMeta); + } + else + { + var craftNode = ConfigNode.Load(craftFile); + meta.LoadDetailsFromCraftFile(craftNode, craftFile); + meta.SaveToMetaFile(craftMeta); + } + int crewCapacity = 0; + vesselName = meta.shipName; + foreach (var partName in meta.partNames) + { + if (SpawnUtils.PartCrewCounts.ContainsKey(partName)) + crewCapacity += SpawnUtils.PartCrewCounts[partName]; + } + return crewCapacity; + } + + IEnumerator ChooseCrew() + { + messageState = Messages.ChoosingCrew; + KerbalNames.Clear(); + ShowCrewSelection(new Vector2(Screen.width / 2, Screen.height / 2)); + while (showCrewSelection) + { + if (Input.GetKeyDown(KeyCode.Escape)) + { + HideCrewSelection(); + break; + } + yield return wait; + } + } + + IEnumerator GetSpawnPoint() + { + messageState = Messages.ChoosingSpawnPoint; + // Use the same indicator as the original VesselMover for familiarity. + GameObject indicatorObject = new GameObject(); + LineRenderer lr = indicatorObject.AddComponent(); + lr.material = new Material(Shader.Find("KSP/Particles/Alpha Blended")); + lr.material.SetColor("_TintColor", Color.green); + lr.material.mainTexture = Texture2D.whiteTexture; + lr.useWorldSpace = false; + + Vector3[] positions = new Vector3[] { Vector3.zero, 10 * Vector3.forward }; + lr.SetPositions(positions); + lr.positionCount = positions.Length; + lr.startWidth = 0.1f; + lr.endWidth = 1f; + + Vector3 mouseAim, point; + Ray ray; + bool altitudeCorrection = false; + var currentMainBody = FlightGlobals.currentMainBody; + while (BDArmorySetup.showVesselMoverGUI) + { + if (Input.GetKeyDown(KeyCode.Escape)) + { + geoCoords = Vector3d.zero; + break; + } + + mouseAim = new Vector3(Input.mousePosition.x / Screen.width, Input.mousePosition.y / Screen.height, 0); + ray = FlightCamera.fetch.mainCamera.ViewportPointToRay(mouseAim); + if (Physics.Raycast(ray, out RaycastHit hit, (ray.origin - currentMainBody.transform.position).magnitude, (int)(LayerMasks.Scenery | LayerMasks.Parts | LayerMasks.Wheels | LayerMasks.EVA))) + { + point = hit.point; + altitudeCorrection = false; + } + else if (SphereRayIntersect(ray, currentMainBody.transform.position, (float)currentMainBody.Radius, out float distance)) + { + point = ray.GetPoint(distance); + altitudeCorrection = true; + } + else + { + yield return wait; + continue; + } + + indicatorObject.transform.position = point; + indicatorObject.transform.rotation = Quaternion.LookRotation(point - currentMainBody.transform.position); + + if (Input.GetMouseButtonDown(0)) + { + currentMainBody.GetLatLonAlt(point, out geoCoords.x, out geoCoords.y, out geoCoords.z); + if (altitudeCorrection) geoCoords.z = currentMainBody.TerrainAltitude(geoCoords.x, geoCoords.y, BDArmorySettings.VESSEL_MOVER_BELOW_WATER); + break; + } + yield return wait; + } + Destroy(indicatorObject); + } + + bool SphereRayIntersect(Ray ray, Vector3 sphereCenter, float sphereRadius, out float distance) + { + distance = 0; + Vector3 n = ray.direction; + Vector3 R = ray.origin - sphereCenter; + float r = sphereRadius; + float d = Vector3.Dot(n, R); // d is non-positive if the ray originates outside the sphere and intersects it + if (d > 0) return false; + var G = d * d - R.sqrMagnitude + r * r; + if (G < 0) return false; + distance = -d - BDAMath.Sqrt(G); + return true; + } + + IEnumerator SpawnVessel(string craftUrl, double latitude, double longitude, double altitude, float initialHeading = 90f, float initialPitch = 0f, HashSet kerbalNames = null) + { + messageState = Messages.LoadingCraft; + spawnFailureReason = SpawnFailureReason.None; // Reset the spawn failure reason. + var spawnPoint = FlightGlobals.currentMainBody.GetWorldSurfacePosition(latitude, longitude, altitude); + var radialUnitVector = (spawnPoint - FlightGlobals.currentMainBody.transform.position).normalized; + var north = VectorUtils.GetNorthVector(spawnPoint, FlightGlobals.currentMainBody); + var direction = (Quaternion.AngleAxis(initialHeading, radialUnitVector) * north).ProjectOnPlanePreNormalized(radialUnitVector).normalized; + var crew = new List(); + if (kerbalNames != null) + { + foreach (var kerbalName in kerbalNames) crew.Add(HighLogic.CurrentGame.CrewRoster[kerbalName]); + VesselSpawner.ReservedCrew = crew.Select(crew => crew.name).ToHashSet(); // Reserve the crew so they don't get swapped out. + foreach (var c in crew) c.rosterStatus = ProtoCrewMember.RosterStatus.Available; // Set all the requested crew as available. + } + VesselSpawnConfig vesselSpawnConfig = new VesselSpawnConfig(craftUrl, spawnPoint, direction, (float)altitude, initialPitch, false, crew: crew); + + // Spawn vessel. + yield return SpawnSingleVessel(vesselSpawnConfig); + VesselSpawner.ReservedCrew.Clear(); // Clear the reserved crew again. + if (spawnFailureReason != SpawnFailureReason.None) { state = State.None; yield break; } + if (!spawnedVessels.TryGetValue(latestSpawnedVesselName, out Vessel vessel) || vessel == null) + { + spawnFailureReason = SpawnFailureReason.VesselFailedToSpawn; + state = State.None; + yield break; + } + if (vesselSpawnConfig.editorFacility == EditorFacility.VAB) vessel.SetRotation(Quaternion.AngleAxis(90, radialUnitVector) * vessel.transform.rotation); // Rotate rockets to the same orientation as the launch pad. + spawnedVessel = vessel; + } + + public override IEnumerator Spawn(SpawnConfig spawnConfig) { yield break; } // Compliance with SpawnStrategy kludge. + #endregion + + #region GUI + static int guiCheckIndex = -1; + bool helpShowing = false; + bool ready = false; + float windowWidth = 300; + enum Messages { None, Custom, OpeningCraftBrowser, ChoosingCrew, ChoosingSpawnPoint, LoadingCraft, EasingCraft } + Messages messageState { get { return _messageState; } set { _messageState = value; if (value != Messages.None) messageDisplayTime = Time.time + 5; } } + Messages _messageState = Messages.None; + string customMessage = ""; + float messageDisplayTime = 0; + + private void OnGUI() + { + if (!(ready && BDArmorySetup.GAME_UI_ENABLED && HighLogic.LoadedSceneIsFlight)) + return; + + if (BDArmorySetup.showVesselMoverGUI) + { + BDArmorySetup.SetGUIOpacity(); + BDArmorySetup.WindowRectVesselMover = GUILayout.Window( + GUIUtility.GetControlID(FocusType.Passive), + BDArmorySetup.WindowRectVesselMover, + WindowVesselMover, + StringUtils.Localize("#LOC_BDArmory_VesselMover_Title"), // "BDA Vessel Mover" + BDArmorySetup.BDGuiSkin.window, + GUILayout.Width(windowWidth) + ); + if (showVesselSelection) + { + vesselSelectionWindowRect = GUILayout.Window( + GUIUtility.GetControlID(FocusType.Passive), + vesselSelectionWindowRect, + VesselSelectionWindow, + StringUtils.Localize("#LOC_BDArmory_Settings_CustomSpawnTemplate_VesselSelection"), + BDArmorySetup.BDGuiSkin.window + ); + } + else if (showCrewSelection) + { + crewSelectionWindowRect = GUILayout.Window( + GUIUtility.GetControlID(FocusType.Passive), + crewSelectionWindowRect, + CrewSelectionWindow, + StringUtils.Localize("#LOC_BDArmory_Settings_CustomSpawnTemplate_CrewSelection"), + BDArmorySetup.BDGuiSkin.window + ); + } + BDArmorySetup.SetGUIOpacity(false); + GUIUtils.UpdateGUIRect(BDArmorySetup.WindowRectVesselMover, guiCheckIndex); + } + else + { + if (showCrewSelection) + { + KerbalNames.Clear(); + HideCrewSelection(); + } + if (showVesselSelection) + { + HideVesselSelection(); + } + } + + if (Time.time > messageDisplayTime) messageState = Messages.None; + switch (messageState) + { + case Messages.Custom: + DrawShadowedMessage(customMessage); + break; + case Messages.OpeningCraftBrowser: + DrawShadowedMessage("Opening Craft Browser..."); + break; + case Messages.ChoosingCrew: + DrawShadowedMessage("Opening Crew Selection..."); + break; + case Messages.ChoosingSpawnPoint: + DrawShadowedMessage("Click somewhere to spawn!"); + break; + case Messages.LoadingCraft: + DrawShadowedMessage("Loading Craft..."); + break; + case Messages.EasingCraft: + DrawShadowedMessage("Easing Craft for up to 10s..."); + break; + } + } + + GUIStyle messageStyle; + GUIStyle messageShadowStyle; + void ConfigureStyles() + { + messageStyle = new GUIStyle(HighLogic.Skin.label); + messageStyle.fontSize = 22; + messageStyle.alignment = TextAnchor.UpperCenter; + + messageShadowStyle = new GUIStyle(messageStyle); + messageShadowStyle.normal.textColor = new Color(0, 0, 0, 0.75f); + } + + void DrawShadowedMessage(string message) + { + Rect labelRect = new Rect(0, (Screen.height * 0.25f) + (Mathf.Sin(2 * Time.time) * 5), Screen.width, 200); + Rect shadowRect = new Rect(labelRect); + shadowRect.position += new Vector2(2, 2); + GUI.Label(shadowRect, message, messageShadowStyle); + GUI.Label(labelRect, message, messageStyle); + } + + void OnVesselChanged(Vessel vessel) + { + if (!IsValid(vessel)) + { state = State.None; } + if (movingVessels.Contains(vessel)) + { state = State.Moving; } + else if (loweringVessels.Contains(vessel)) + { state = State.Lowering; } + else + { state = State.None; } + // Clean the moving and lowering vessel hashsets. + movingVessels = movingVessels.Where(v => IsValid(v)).ToHashSet(); + loweringVessels = loweringVessels.Where(v => IsValid(v)).ToHashSet(); + } + + void WindowVesselMover(int id) + { + GUI.DragWindow(new Rect(0, 0, BDArmorySetup.WindowRectVesselMover.width - 24, 24)); + if (GUI.Button(new Rect(BDArmorySetup.WindowRectVesselMover.width - 24, 0, 24, 24), " X", BDArmorySetup.CloseButtonStyle)) SetVisible(false); + GUILayout.BeginVertical(GUILayout.ExpandHeight(true)); + switch (state) + { + case State.None: + { + if (GUILayout.Button(StringUtils.Localize("#LOC_BDArmory_VesselMover_MoveVessel"), BDArmorySetup.ButtonStyle, GUILayout.Height(40))) StartCoroutine(MoveVessel(FlightGlobals.ActiveVessel)); + if (GUILayout.Button(StringUtils.Localize("#LOC_BDArmory_VesselMover_SpawnVessel"), BDArmorySetup.ButtonStyle, GUILayout.Height(40))) StartCoroutine(SpawnVessel()); + if (GUILayout.Button(StringUtils.Localize("#LOC_BDArmory_VesselMover_RecoverVessel"), BDArmorySetup.ButtonStyle, GUILayout.Height(20))) RecoverVessel(); + GUILayout.BeginHorizontal(); + BDArmorySettings.VESSEL_MOVER_CHOOSE_CREW = GUILayout.Toggle(BDArmorySettings.VESSEL_MOVER_CHOOSE_CREW, StringUtils.Localize("#LOC_BDArmory_VesselMover_ChooseCrew")); + BDArmorySettings.VESSEL_MOVER_CLASSIC_CRAFT_CHOOSER = GUILayout.Toggle(BDArmorySettings.VESSEL_MOVER_CLASSIC_CRAFT_CHOOSER, StringUtils.Localize("#LOC_BDArmory_VesselMover_ClassicChooser")); + GUILayout.EndHorizontal(); + GUILayout.BeginHorizontal(); + BDArmorySettings.VESSEL_MOVER_CLOSE_ON_COMPETITION_START = GUILayout.Toggle(BDArmorySettings.VESSEL_MOVER_CLOSE_ON_COMPETITION_START, StringUtils.Localize("#LOC_BDArmory_VesselMover_CloseOnCompetitionStart")); + GUILayout.EndHorizontal(); + break; + } + case State.Moving: + { + if (GUILayout.Button(StringUtils.Localize("#LOC_BDArmory_VesselMover_PlaceVessel"), BDArmorySetup.ButtonStyle, GUILayout.Height(40))) StartCoroutine(PlaceVessel(FlightGlobals.ActiveVessel)); + if (GUILayout.Button(StringUtils.Localize("#LOC_BDArmory_VesselMover_DropVessel"), BDArmorySetup.ButtonStyle, GUILayout.Height(40))) DropVessel(FlightGlobals.ActiveVessel); + GUILayout.BeginHorizontal(); + GUILayout.BeginVertical(); + BDArmorySettings.VESSEL_MOVER_ENABLE_BRAKES = GUILayout.Toggle(BDArmorySettings.VESSEL_MOVER_ENABLE_BRAKES, StringUtils.Localize("#LOC_BDArmory_VesselMover_EnableBrakes")); + BDArmorySettings.VESSEL_MOVER_LOWER_FAST = GUILayout.Toggle(BDArmorySettings.VESSEL_MOVER_LOWER_FAST, StringUtils.Localize("#LOC_BDArmory_VesselMover_LowerFast")); + BDArmorySettings.VESSEL_MOVER_DONT_WORRY_ABOUT_COLLISIONS = GUILayout.Toggle(BDArmorySettings.VESSEL_MOVER_DONT_WORRY_ABOUT_COLLISIONS, StringUtils.Localize("#LOC_BDArmory_VesselMover_DontWorryAboutCollisions")); + GUILayout.EndVertical(); + GUILayout.BeginVertical(); + BDArmorySettings.VESSEL_MOVER_ENABLE_SAS = GUILayout.Toggle(BDArmorySettings.VESSEL_MOVER_ENABLE_SAS, StringUtils.Localize("#LOC_BDArmory_VesselMover_EnableSAS")); + BDArmorySettings.VESSEL_MOVER_BELOW_WATER = GUILayout.Toggle(BDArmorySettings.VESSEL_MOVER_BELOW_WATER, StringUtils.Localize("#LOC_BDArmory_VesselMover_BelowWater")); + GUILayout.EndVertical(); + GUILayout.EndHorizontal(); + GUILayout.BeginHorizontal(); + GUILayout.Label($"{StringUtils.Localize("#LOC_BDArmory_VesselMover_MinLowerSpeed")}: {BDArmorySettings.VESSEL_MOVER_MIN_LOWER_SPEED}", GUILayout.Width(130)); + BDArmorySettings.VESSEL_MOVER_MIN_LOWER_SPEED = BDAMath.RoundToUnit(GUILayout.HorizontalSlider(BDArmorySettings.VESSEL_MOVER_MIN_LOWER_SPEED, 0.1f, 1f), 0.1f); + GUILayout.EndHorizontal(); + if (GUILayout.Button(StringUtils.Localize("#LOC_BDArmory_Generic_Help"), helpShowing ? BDArmorySetup.SelectedButtonStyle : BDArmorySetup.ButtonStyle, GUILayout.Height(20))) + { + helpShowing = !helpShowing; + if (!helpShowing) ResetWindowHeight(); + } + if (helpShowing) + { + GUILayout.BeginVertical(); + GUILayout.Label($"{StringUtils.Localize("#LOC_BDArmory_VesselMover_Help_Movement")}: {GameSettings.PITCH_DOWN.primary}, {GameSettings.PITCH_UP.primary}, {GameSettings.YAW_LEFT.primary}, {GameSettings.YAW_RIGHT.primary}"); + GUILayout.Label($"{StringUtils.Localize("#LOC_BDArmory_VesselMover_Help_Roll")}: {GameSettings.ROLL_LEFT.primary}, {GameSettings.ROLL_RIGHT.primary}"); + GUILayout.Label($"{StringUtils.Localize("#LOC_BDArmory_VesselMover_Help_Pitch")}: {GameSettings.TRANSLATE_DOWN.primary}, {GameSettings.TRANSLATE_UP.primary}"); + GUILayout.Label($"{StringUtils.Localize("#LOC_BDArmory_VesselMover_Help_Yaw")}: {GameSettings.TRANSLATE_LEFT.primary}, {GameSettings.TRANSLATE_RIGHT.primary}"); + GUILayout.Label($"{StringUtils.Localize("#LOC_BDArmory_VesselMover_Help_AutoRotateRocket")}: {GameSettings.TRANSLATE_BACK.primary}"); + GUILayout.Label($"{StringUtils.Localize("#LOC_BDArmory_VesselMover_Help_AutoRotatePlane")}: {GameSettings.TRANSLATE_FWD.primary}"); + GUILayout.Label(StringUtils.Localize("#LOC_BDArmory_VesselMover_Help_CycleAltitudes")); + GUILayout.Label($"{StringUtils.Localize("#LOC_BDArmory_VesselMover_Help_ResetAltitude")}: {GameSettings.THROTTLE_CUTOFF.primary}"); + GUILayout.Label($"{StringUtils.Localize("#LOC_BDArmory_VesselMover_Help_AdjustAltitude")}: {GameSettings.THROTTLE_UP.primary}, {GameSettings.THROTTLE_DOWN.primary}"); + GUILayout.EndVertical(); + } + break; + } + case State.Lowering: + { + if (GUILayout.Button(StringUtils.Localize("#LOC_BDArmory_VesselMover_MoveVessel"), BDArmorySetup.ButtonStyle, GUILayout.Height(40))) { DropVessel(FlightGlobals.ActiveVessel); StartCoroutine(MoveVessel(FlightGlobals.ActiveVessel)); } + if (GUILayout.Button(StringUtils.Localize("#LOC_BDArmory_VesselMover_DropVessel"), BDArmorySetup.ButtonStyle, GUILayout.Height(40))) DropVessel(FlightGlobals.ActiveVessel); + GUILayout.BeginHorizontal(); + GUILayout.BeginVertical(); + BDArmorySettings.VESSEL_MOVER_ENABLE_BRAKES = GUILayout.Toggle(BDArmorySettings.VESSEL_MOVER_ENABLE_BRAKES, StringUtils.Localize("#LOC_BDArmory_VesselMover_EnableBrakes")); + BDArmorySettings.VESSEL_MOVER_LOWER_FAST = GUILayout.Toggle(BDArmorySettings.VESSEL_MOVER_LOWER_FAST, StringUtils.Localize("#LOC_BDArmory_VesselMover_LowerFast")); + GUILayout.EndVertical(); + GUILayout.BeginVertical(); + BDArmorySettings.VESSEL_MOVER_ENABLE_SAS = GUILayout.Toggle(BDArmorySettings.VESSEL_MOVER_ENABLE_SAS, StringUtils.Localize("#LOC_BDArmory_VesselMover_EnableSAS")); + BDArmorySettings.VESSEL_MOVER_BELOW_WATER = GUILayout.Toggle(BDArmorySettings.VESSEL_MOVER_BELOW_WATER, StringUtils.Localize("#LOC_BDArmory_VesselMover_BelowWater")); + GUILayout.EndVertical(); + GUILayout.EndHorizontal(); + GUILayout.BeginHorizontal(); + GUILayout.Label($"{StringUtils.Localize("#LOC_BDArmory_VesselMover_MinLowerSpeed")}: {BDArmorySettings.VESSEL_MOVER_MIN_LOWER_SPEED}", GUILayout.Width(130)); + BDArmorySettings.VESSEL_MOVER_MIN_LOWER_SPEED = BDAMath.RoundToUnit(GUILayout.HorizontalSlider(BDArmorySettings.VESSEL_MOVER_MIN_LOWER_SPEED, 0.1f, 1f), 0.1f); + GUILayout.EndHorizontal(); + break; + } + case State.Spawning: + { + GUILayout.Label($"Spawning craft...", BDArmorySetup.SelectedButtonStyle, GUILayout.Height(40)); + GUILayout.BeginHorizontal(); + BDArmorySettings.VESSEL_MOVER_CHOOSE_CREW = GUILayout.Toggle(BDArmorySettings.VESSEL_MOVER_CHOOSE_CREW, StringUtils.Localize("#LOC_BDArmory_VesselMover_ChooseCrew")); + BDArmorySettings.VESSEL_MOVER_PLACE_AFTER_SPAWN = GUILayout.Toggle(BDArmorySettings.VESSEL_MOVER_PLACE_AFTER_SPAWN, StringUtils.Localize("#LOC_BDArmory_VesselMover_PlaceAfterSpawn")); + GUILayout.EndHorizontal(); + break; + } + } + GUILayout.EndVertical(); + GUIUtils.RepositionWindow(ref BDArmorySetup.WindowRectVesselMover); + GUIUtils.UpdateGUIRect(BDArmorySetup.WindowRectVesselMover, guiCheckIndex); + GUIUtils.UseMouseEventInRect(BDArmorySetup.WindowRectVesselMover); + } + + /// + /// Reset the height of the window so that it shrinks. + /// + void ResetWindowHeight() + { + bool reposition = BDArmorySetup.WindowRectVesselMover.y + BDArmorySetup.WindowRectVesselMover.height == Screen.height; + BDArmorySetup.WindowRectVesselMover.height = 0; + if (reposition) BDArmorySetup.WindowRectVesselMover.y = Screen.height; + GUIUtils.RepositionWindow(ref BDArmorySetup.WindowRectVesselMover); + } + + public void SetVisible(bool visible) + { + if (!visible) + { + if (state == State.Spawning) + { + abortCraftSelection = true; + state = State.None; + } + craftBrowser = null; // Make sure the craft browser is cleaned up. + } + BDArmorySetup.showVesselMoverGUI = visible; + GUIUtils.SetGUIRectVisible(guiCheckIndex, visible); + if (button != null) + { + if (visible) button.SetTrue(false); + else button.SetFalse(false); + } + } + void ShowVMGUI() => SetVisible(true); + void HideVMGUI() => SetVisible(false); + + #region Vessel Selection + internal static int _vesselGUICheckIndex = -1; + bool showVesselSelection = false; + Rect vesselSelectionWindowRect = new Rect(0, 0, 600, 800); + Vector2 vesselSelectionScrollPos = default; + float vesselSelectionTimer = 0; + string selectedVesselURL = ""; + string selectionFilter = ""; + bool focusFilterField = false; // Focus the filter text field. + + public void ShowVesselSelection(Action selectedCallback = null, Action cancelledCallback = null) + { + if (craftBrowser == null) + { + craftBrowser = new CustomCraftBrowserDialog(); + craftBrowser.UpdateList(craftBrowser.facility); + } + craftBrowser.selectFileCallback = selectedCallback; + craftBrowser.cancelledCallback = cancelledCallback; + vesselSelectionWindowRect.position = new Vector2((Screen.width - vesselSelectionWindowRect.width) / 2, (Screen.height - vesselSelectionWindowRect.height) / 2); + selectedVesselURL = ""; + showVesselSelection = true; + focusFilterField = true; + vesselSelectionTimer = Time.realtimeSinceStartup; + GUIUtils.SetGUIRectVisible(_vesselGUICheckIndex, true); + } + + public void HideVesselSelection() + { + showVesselSelection = false; + GUIUtils.SetGUIRectVisible(_vesselGUICheckIndex, false); + } + + public void VesselSelectionWindow(int windowID) + { + GUI.DragWindow(new Rect(0, 0, vesselSelectionWindowRect.width, 20)); + GUILayout.BeginVertical(); + selectionFilter = GUIUtils.TextField(selectionFilter, " Filter", "VMFilterField"); + if (focusFilterField) + { + GUI.FocusControl("VMFilterField"); + focusFilterField = false; + } + vesselSelectionScrollPos = GUILayout.BeginScrollView(vesselSelectionScrollPos, GUI.skin.box, GUILayout.Width(vesselSelectionWindowRect.width - 15), GUILayout.MaxHeight(vesselSelectionWindowRect.height - 60)); + using (var vessels = craftBrowser.craftList.GetEnumerator()) + while (vessels.MoveNext()) + { + var vesselURL = vessels.Current.Key; + var vesselInfo = vessels.Current.Value; + if (vesselURL == null || vesselInfo == null) continue; + if (!string.IsNullOrEmpty(selectionFilter)) // Filter selection, case insensitive. + { + if (!vesselInfo.shipName.ToLower().Contains(selectionFilter.ToLower())) continue; + } + GUILayout.BeginHorizontal(); // Vessel buttons + if (GUILayout.Button($"{vesselInfo.shipName}", selectedVesselURL == vesselURL ? BDArmorySetup.SelectedButtonStyle : BDArmorySetup.ButtonStyle, GUILayout.MaxHeight(60), GUILayout.MaxWidth(vesselSelectionWindowRect.width - 190))) + { + if (Time.realtimeSinceStartup - vesselSelectionTimer < 0.5f) + { + if (craftBrowser.selectFileCallback != null) craftBrowser.selectFileCallback(vesselURL); + HideVesselSelection(); + } + else if (selectedVesselURL == vesselURL) { selectedVesselURL = ""; } + else { selectedVesselURL = vesselURL; } + vesselSelectionTimer = Time.realtimeSinceStartup; + } + GUILayout.Label($"{StringUtils.Localize("#LOC_BDArmory_Settings_CustomSpawnTemplate_Parts")}: {vesselInfo.partCount}, {StringUtils.Localize("#LOC_BDArmory_Settings_CustomSpawnTemplate_Mass")}: {(vesselInfo.totalMass < 1000f ? $"{vesselInfo.totalMass:G3}t" : $"{vesselInfo.totalMass / 1000f:G3}kt")}\nCrew count: {(craftBrowser.crewCounts.ContainsKey(vesselURL) ? craftBrowser.crewCounts[vesselURL].ToString() : "unknown")}\n{(vesselInfo.UnavailableShipParts.Count > 0 ? $"{StringUtils.Localize("#LOC_BDArmory_Settings_CustomSpawnTemplate_InvalidParts")}" : $"{StringUtils.Localize("#LOC_BDArmory_Settings_CustomSpawnTemplate_Version")}: {(vesselInfo.compatibility == VersionCompareResult.COMPATIBLE ? $"{vesselInfo.version}" : $"{vesselInfo.version}")}{(vesselInfo.UnavailableShipPartModules.Count > 0 ? $" {StringUtils.Localize("#LOC_BDArmory_Settings_CustomSpawnTemplate_UnknownModules")}" : "")}")}", CustomCraftBrowserDialog.vesselInfoStyle); + GUILayout.EndHorizontal(); + } + GUILayout.EndScrollView(); + GUILayout.Space(10); + GUILayout.BeginHorizontal(); + if (GUILayout.Button(StringUtils.Localize("#LOC_BDArmory_Generic_Select"), selectedVesselURL != "" ? BDArmorySetup.ButtonStyle : BDArmorySetup.SelectedButtonStyle) && selectedVesselURL != "") + { + if (craftBrowser.selectFileCallback != null) craftBrowser.selectFileCallback(selectedVesselURL); + HideVesselSelection(); + } + if (GUILayout.Button(StringUtils.Localize("#LOC_BDArmory_Generic_Cancel"), BDArmorySetup.ButtonStyle)) + { + if (craftBrowser.cancelledCallback != null) craftBrowser.cancelledCallback(); + HideVesselSelection(); + } + if (GUILayout.Button(craftBrowser.facility == EditorFacility.SPH ? "VAB" : "SPH", BDArmorySetup.ButtonStyle, GUILayout.Width(vesselSelectionWindowRect.width / 6))) + { + craftBrowser.facility = (craftBrowser.facility == EditorFacility.SPH ? EditorFacility.VAB : EditorFacility.SPH); + craftBrowser.UpdateList(craftBrowser.facility); + } + if (GUILayout.Button(StringUtils.Localize("#LOC_BDArmory_Settings_CustomSpawnTemplate_Refresh"), BDArmorySetup.ButtonStyle, GUILayout.Width(vesselSelectionWindowRect.width / 6))) + { craftBrowser.UpdateList(craftBrowser.facility); } + GUILayout.EndHorizontal(); + GUILayout.EndVertical(); + GUIUtils.RepositionWindow(ref vesselSelectionWindowRect); + GUIUtils.UpdateGUIRect(vesselSelectionWindowRect, _vesselGUICheckIndex); + GUIUtils.UseMouseEventInRect(vesselSelectionWindowRect); + } + + #endregion + + #region Crew Selection + internal static int _crewGUICheckIndex = -1; + bool showCrewSelection = false; + Rect crewSelectionWindowRect = new Rect(0, 0, 300, 400); + Vector2 crewSelectionScrollPos = default; + float crewSelectionTimer = 0; + HashSet ActiveCrewMembers = new HashSet(); + bool newCustomKerbal = false; + string newKerbalName = ""; + bool focusKerbalNameField = false; // Focus the kerbal name field. + bool notThisFrame = true; // Delay for dynamically added text field. + ProtoCrewMember.Gender newKerbalGender = ProtoCrewMember.Gender.Male; + bool removeKerbals = false; + + /// + /// Show the crew selection window. + /// + /// Position of the mouse click. + /// The VesselSpawnConfig clicked on. + public void ShowCrewSelection(Vector2 position) + { + crewSelectionWindowRect.position = position + new Vector2(50, -crewSelectionWindowRect.height / 2); // Centred and slightly offset to allow clicking the same spot. + showCrewSelection = true; + // Find any crew on active vessels. + ActiveCrewMembers.Clear(); + foreach (var vessel in FlightGlobals.Vessels) + { + if (vessel == null || !vessel.loaded) continue; + foreach (var part in vessel.Parts) + { + if (part == null) continue; + foreach (var crew in part.protoModuleCrew) + { + if (crew == null) continue; + ActiveCrewMembers.Add(crew.name); + } + } + } + GUIUtils.SetGUIRectVisible(_crewGUICheckIndex, true); + foreach (var crew in HighLogic.CurrentGame.CrewRoster.Kerbals(ProtoCrewMember.KerbalType.Crew)) // Set any non-assigned crew as available. + { + if (crew.rosterStatus != ProtoCrewMember.RosterStatus.Assigned) + crew.rosterStatus = ProtoCrewMember.RosterStatus.Available; + } + crewSelectionTimer = Time.realtimeSinceStartup; + } + + /// + /// Hide the crew selection window. + /// + public void HideCrewSelection() + { + showCrewSelection = false; + newCustomKerbal = false; + removeKerbals = false; + GUIUtils.SetGUIRectVisible(_crewGUICheckIndex, false); + } + + /// + /// Crew selection window. + /// + /// + public void CrewSelectionWindow(int windowID) + { + KerbalRoster kerbalRoster = HighLogic.CurrentGame.CrewRoster; + GUI.DragWindow(new Rect(0, 0, crewSelectionWindowRect.width, 20)); + GUILayout.BeginVertical(); + if (BDArmorySettings.VESSEL_SPAWN_FILL_SEATS == 0) GUILayout.Label($"Select up to {crewCapacity} kerbals to populate {vesselNameToSpawn}."); + crewSelectionScrollPos = GUILayout.BeginScrollView(crewSelectionScrollPos, GUI.skin.box, GUILayout.Width(crewSelectionWindowRect.width - 15), GUILayout.MaxHeight(crewSelectionWindowRect.height - 60)); + using (var kerbals = kerbalRoster.Kerbals(ProtoCrewMember.KerbalType.Crew).GetEnumerator()) + while (kerbals.MoveNext()) + { + ProtoCrewMember crewMember = kerbals.Current; + if (crewMember == null || ActiveCrewMembers.Contains(crewMember.name)) continue; + if (GUILayout.Button($"{crewMember.name}, {crewMember.gender}, {crewMember.trait}", KerbalNames.Contains(crewMember.name) ? BDArmorySetup.SelectedButtonStyle : BDArmorySetup.ButtonStyle)) + { + if (Time.realtimeSinceStartup - crewSelectionTimer < 0.5f) + { + KerbalNames.Add(crewMember.name); + HideCrewSelection(); + } + else if (KerbalNames.Contains(crewMember.name)) KerbalNames.Remove(crewMember.name); + else KerbalNames.Add(crewMember.name); + crewSelectionTimer = Time.realtimeSinceStartup; + } + } + GUILayout.EndScrollView(); + GUILayout.Space(10); + GUILayout.BeginHorizontal(); + if (GUILayout.Button(StringUtils.Localize("#LOC_BDArmory_Generic_Select"), BDArmorySetup.ButtonStyle)) + { HideCrewSelection(); } + if (GUILayout.Button(StringUtils.Localize("#LOC_BDArmory_VesselMover_Any"), BDArmorySetup.ButtonStyle, GUILayout.Width(crewSelectionWindowRect.width / 6))) + { KerbalNames.Clear(); HideCrewSelection(); } + if (GUILayout.Button(StringUtils.Localize("#LOC_BDArmory_Generic_New"), newCustomKerbal ? BDArmorySetup.SelectedButtonStyle : BDArmorySetup.ButtonStyle, GUILayout.Width(crewSelectionWindowRect.width / 6))) + { + // Create a new Kerbal! + newCustomKerbal = !newCustomKerbal; + newKerbalName = ""; + focusKerbalNameField = newCustomKerbal; + notThisFrame = true; + } + if (GUILayout.Button("X", removeKerbals ? BDArmorySetup.SelectedButtonStyle : BDArmorySetup.CloseButtonStyle, GUILayout.Width(27))) + { + // Remove selected Kerbals! + removeKerbals = !removeKerbals; + } + GUILayout.EndHorizontal(); + if (newCustomKerbal) + { + newKerbalName = GUIUtils.TextField(newKerbalName, " Enter a new Kerbal name...", "kerbalNameField"); + if (!notThisFrame && focusKerbalNameField) + { + GUI.FocusControl("kerbalNameField"); + focusKerbalNameField = false; + } + if (notThisFrame) notThisFrame = false; + GUILayout.BeginHorizontal(); + if (GUILayout.Button(StringUtils.Localize("#LOC_BDArmory_Generic_OK"), BDArmorySetup.ButtonStyle)) + { + newKerbalName = newKerbalName.Trim(); + if (!string.IsNullOrEmpty(newKerbalName)) + { + if (HighLogic.CurrentGame.CrewRoster.Exists(newKerbalName)) + { + customMessage = $"Failed to add {newKerbalName}. They already exist!"; + messageState = Messages.Custom; + Debug.LogWarning($"[BDArmory.VesselMover]: {customMessage}"); + } + else + { + var crewMember = HighLogic.CurrentGame.CrewRoster.GetNewKerbal(ProtoCrewMember.KerbalType.Crew); + if (crewMember.ChangeName(newKerbalName)) + { + crewMember.gender = newKerbalGender; + KerbalRoster.SetExperienceTrait(crewMember, KerbalRoster.pilotTrait); // Make the kerbal a pilot (so they can use SAS properly). + KerbalRoster.SetExperienceLevel(crewMember, KerbalRoster.GetExperienceMaxLevel()); // Make them experienced. + crewMember.isBadass = true; // Make them bad-ass (likes nearby explosions). + } + else + { + customMessage = $"Failed to set name of {crewMember.name} to {newKerbalName}"; + messageState = Messages.Custom; + Debug.LogWarning($"[BDArmory.VesselMover]: {customMessage}"); + HighLogic.CurrentGame.CrewRoster.Remove(crewMember.name); + } + } + } + newCustomKerbal = false; + } + if (GUILayout.Button(newKerbalGender.ToStringCached(), BDArmorySetup.ButtonStyle, GUILayout.Width(crewSelectionWindowRect.width / 4))) + { + var genders = Enum.GetValues(typeof(ProtoCrewMember.Gender)).Cast(); + bool found = false, set = false; + foreach (var gender in genders) + { + if (found) { newKerbalGender = gender; set = true; break; } + if (newKerbalGender == gender) found = true; + } + if (!set) newKerbalGender = genders.First(); + } + GUILayout.EndHorizontal(); + } + if (removeKerbals) + { + if (GUILayout.Button(StringUtils.Localize("#LOC_BDArmory_VesselMover_ReallyRemoveKerbals"), BDArmorySetup.CloseButtonStyle)) + { + var cantRemove = KerbalRoster.GenerateInitialCrewRoster(HighLogic.CurrentGame.Mode).Crew.Select(crew => crew.name).ToHashSet(); + KerbalNames = KerbalNames.Where(kerbal => !cantRemove.Contains(kerbal)).ToHashSet(); + customMessage = $"Removing {string.Join(", ", KerbalNames)}"; + messageState = Messages.Custom; + foreach (var kerbalName in KerbalNames) HighLogic.CurrentGame.CrewRoster.Remove(kerbalName); + KerbalNames.Clear(); + removeKerbals = false; + } + } + GUILayout.EndVertical(); + GUIUtils.RepositionWindow(ref crewSelectionWindowRect); + GUIUtils.UpdateGUIRect(crewSelectionWindowRect, _crewGUICheckIndex); + GUIUtils.UseMouseEventInRect(crewSelectionWindowRect); + } + #endregion + + const int circleRes = 24; + private LineRenderer moveIndicator; + Vector3[] moveIndicatorPositions = new Vector3[circleRes + 3]; + private void DrawMovingIndicator() + { + var vessel = FlightGlobals.ActiveVessel; + if (vessel == null || !vessel.loaded || vessel.packed) return; + + var angle = 360f / circleRes; + var radius = 2f + vessel.GetRadius(); + var centre = vessel.CoM; + VectorUtils.GetWorldCoordinateFrame(vessel.mainBody, centre, out Vector3 up, out Vector3 north, out Vector3 right); + + moveIndicatorPositions[0] = centre + radius * north; + for (int i = 1; i < circleRes; i++) + { + moveIndicatorPositions[i] = centre + Quaternion.AngleAxis(i * angle, up) * north * radius; + } + moveIndicatorPositions[circleRes] = centre + radius * north; + moveIndicatorPositions[circleRes + 1] = centre; + moveIndicatorPositions[circleRes + 2] = centre + RadarAltitude(vessel) * -up; + + moveIndicator.SetPositions(moveIndicatorPositions); + } + #endregion + + #region Toolbar button + public void AddToolbarButton() + { + if (!HighLogic.LoadedSceneIsFlight) return; + StartCoroutine(ToolbarButtonRoutine()); + } + public void RemoveToolbarButton() + { + if (button == null) return; + if (!HighLogic.LoadedSceneIsFlight) return; + ApplicationLauncher.Instance.RemoveModApplication(button); + button = null; + buttonSetup = false; + } + + IEnumerator ToolbarButtonRoutine() + { + if (buttonSetup) yield break; + if (!HighLogic.LoadedSceneIsFlight && !HighLogic.LoadedSceneIsEditor) yield break; + yield return new WaitUntil(() => ApplicationLauncher.Ready && BDArmorySetup.toolbarButtonAdded); // Wait until after the main BDA toolbar button. + + if (!buttonSetup) + { + Texture buttonTexture = GameDatabase.Instance.GetTexture(BDArmorySetup.textureDir + "icon_vm", false); + button = ApplicationLauncher.Instance.AddModApplication(ShowVMGUI, HideVMGUI, Dummy, Dummy, Dummy, Dummy, ApplicationLauncher.AppScenes.FLIGHT, buttonTexture); + buttonSetup = true; + if (BDArmorySetup.showVesselMoverGUI) button.SetTrue(false); + } + } + void Dummy() { } + #endregion + } +} \ No newline at end of file diff --git a/BDArmory/Competition/VesselSpawning/VesselLoader.cs b/BDArmory/VesselSpawning/VesselSpawner.cs similarity index 68% rename from BDArmory/Competition/VesselSpawning/VesselLoader.cs rename to BDArmory/VesselSpawning/VesselSpawner.cs index 16d02b4d3..dad82bb24 100644 --- a/BDArmory/Competition/VesselSpawning/VesselLoader.cs +++ b/BDArmory/VesselSpawning/VesselSpawner.cs @@ -1,12 +1,14 @@ using UnityEngine; using System; using System.Collections.Generic; +using System.IO; using System.Linq; -using BDArmory.Core; -using BDArmory.Modules; +using BDArmory.Control; +using BDArmory.Settings; +using BDArmory.Weapons; -namespace BDArmory.Competition.VesselSpawning +namespace BDArmory.VesselSpawning { /// /// A static class for doing the actual spawning of a vessel from a craft file into KSP. @@ -16,8 +18,42 @@ namespace BDArmory.Competition.VesselSpawning /// position and orientation of the vessel can be finally assigned. /// Note: KSP sometimes packs and unpacks vessels between frames (possibly due to external seats), which can reset positions and rotations and reset things! /// - public static class VesselLoader + public static class VesselSpawner { + public static string spawnProbeLocation + { + get + { + if (_spawnProbeLocation != null) return _spawnProbeLocation; + _spawnProbeLocation = Path.Combine(KSPUtil.ApplicationRootPath, "GameData", "BDArmory", "craft", "SpawnProbe.craft"); // SpaceDock location + if (!File.Exists(_spawnProbeLocation)) _spawnProbeLocation = Path.Combine(KSPUtil.ApplicationRootPath, "Ships", "SPH", "SpawnProbe.craft"); // CKAN location + if (!File.Exists(_spawnProbeLocation)) + { + _spawnProbeLocation = null; + var message = "SpawnProbe.craft is missing. Your installation is likely corrupt."; + ScreenMessages.PostScreenMessage(message, 10); + Debug.LogError("[BDArmory.SpawnUtils]: " + message); + } + return _spawnProbeLocation; + } + } + private static string _spawnProbeLocation = null; + + /// + /// Spawn a spawn-probe at the camera's coordinates (plus offset). + /// + /// The spawn probe on success, else null. + public static Vessel SpawnSpawnProbe(Vector3 offset = default) + { + // Spawn in the SpawnProbe at the camera position. + var dummyVar = EditorFacility.None; + Vector3d dummySpawnCoords; + FlightGlobals.currentMainBody.GetLatLonAlt(FlightCamera.fetch.transform.position + offset, out dummySpawnCoords.x, out dummySpawnCoords.y, out dummySpawnCoords.z); + if (spawnProbeLocation == null) return null; + Vessel spawnProbe = VesselSpawner.SpawnVesselFromCraftFile(spawnProbeLocation, dummySpawnCoords, 0f, 0f, 0f, out dummyVar); + return spawnProbe; + } + /// /// Spawn a craft at the given coordinates with the given orientation. /// Note: This does not take into account control point orientation, which only exists once the reference transform for the vessel is loaded (not the protovessel here). See the class description for details. @@ -50,9 +86,17 @@ public static Vessel SpawnVesselFromCraftFile(string craftURL, Vector3d gpsCoord newData.crew = new List(); - return SpawnVessel(newData, out shipFacility, crewData); + var vessel = SpawnVessel(newData, out shipFacility, crewData); + SpawnUtils.RestoreKAL(vessel, BDArmorySettings.RESTORE_KAL); + SpawnUtils.OnVesselReady(vessel); + return vessel; } + // Crew reserved for spawning in specific craft. + // Make sure to reserve any crew you don't want randomly spawned! + // Then after spawning all the craft, clear ReservedCrew again to avoid reserving them for the next batch spawn. + public static HashSet ReservedCrew = new HashSet(); + static Vessel SpawnVessel(VesselData vesselData, out EditorFacility shipFacility, List crewData = null) { shipFacility = EditorFacility.None; @@ -114,25 +158,33 @@ static Vessel SpawnVessel(VesselData vesselData, out EditorFacility shipFacility } // Add crew - List crewParts; + List crewParts; // Cockpits, combat seats, command seats, crewable weapons, in this order. ModuleWeapon crewedWeapon; switch (BDArmorySettings.VESSEL_SPAWN_FILL_SEATS) { case 0: // Minimal plus crewable weapons. { - crewParts = shipConstruct.parts.FindAll(p => p.protoModuleCrew.Count < p.CrewCapacity && (crewedWeapon = p.FindModuleImplementing()) && crewedWeapon.crewserved).ToList(); // Crewed weapons. - var part = shipConstruct.parts.Find(p => p.protoModuleCrew.Count < p.CrewCapacity && !p.FindModuleImplementing() && (p.FindModuleImplementing() || p.FindModuleImplementing())); // A non-weapon crewed command part. + crewParts = new List(); + var part = shipConstruct.parts.Find(p => p.protoModuleCrew.Count < p.CrewCapacity && p.FindModuleImplementing()); // A cockpit. + if (part == null) part = shipConstruct.parts.Find(p => p.protoModuleCrew.Count < p.CrewCapacity && p.FindModuleImplementing() && p.FindModuleImplementing()); // A combat seat. + if (part == null) part = shipConstruct.parts.Find(p => p.protoModuleCrew.Count < p.CrewCapacity && p.FindModuleImplementing()); // A command seat. if (part) crewParts.Add(part); + crewParts.AddRange(shipConstruct.parts.FindAll(p => p.protoModuleCrew.Count < p.CrewCapacity && (crewedWeapon = p.FindModuleImplementing()) && crewedWeapon.crewserved)); // Crewable weapons. break; } case 1: // All cockpits or the first combat seat if no cockpits are found, plus crewable weapons. { crewParts = shipConstruct.parts.FindAll(p => p.protoModuleCrew.Count < p.CrewCapacity && p.FindModuleImplementing()).ToList(); // Crewable cockpits. - if (crewParts.Count() == 0) + if (crewParts.Count() == 0) // No crewable cockpits. { var part = shipConstruct.parts.Find(p => p.protoModuleCrew.Count < p.CrewCapacity && p.FindModuleImplementing() && p.FindModuleImplementing()); // The first combat seat if no cockpits were found. if (part) crewParts.Add(part); } + if (crewParts.Count() == 0) // No crewable combat seats either. + { + var part = shipConstruct.parts.Find(p => p.protoModuleCrew.Count < p.CrewCapacity && p.FindModuleImplementing()); // The first command seat if no cockpits or combat seats were found. + if (part) crewParts.Add(part); + } crewParts.AddRange(shipConstruct.parts.FindAll(p => p.protoModuleCrew.Count < p.CrewCapacity && ((crewedWeapon = p.FindModuleImplementing()) && crewedWeapon.crewserved))); // Crewable weapons. break; } @@ -151,21 +203,78 @@ static Vessel SpawnVessel(VesselData vesselData, out EditorFacility shipFacility } if (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 42) // Fly the Unfriendly Skies { crewParts = shipConstruct.parts.FindAll(p => p.protoModuleCrew.Count < p.CrewCapacity).ToList(); } + List reservedCrew = new List(); + if (crewData != null) // Sanity checks to avoid assigning duplicate or otherwise unavailable crew. + { + crewData = crewData.Where(crew => crew != null && !string.IsNullOrEmpty(crew.name)).ToList(); // Remove null / no-name crew. + if (crewData.Any(crew => !ReservedCrew.Contains(crew.name))) // Remove non-reserved crew. + { + Debug.LogWarning($"[BDArmory.VesselSpawner]: Removing specified, but not reserved crew to avoid potential collisions: {string.Join(", ", crewData.Where(crew => !ReservedCrew.Contains(crew.name)).Select(crew => crew.name))}"); + crewData = crewData.Where(crew => ReservedCrew.Contains(crew.name)).ToList(); + } + if (crewData.Any(crew => crew.rosterStatus == ProtoCrewMember.RosterStatus.Assigned)) // Remove already assigned crew. + { + Debug.LogWarning($"[BDArmory.VesselSpawner]: Removing already assigned crew: {string.Join(", ", crewData.Where(crew => crew.rosterStatus == ProtoCrewMember.RosterStatus.Assigned).Select(crew => crew.name))}"); + crewData = crewData.Where(crew => crew.rosterStatus != ProtoCrewMember.RosterStatus.Assigned).ToList(); + } + foreach (var crew in crewData.Where(crew => !HighLogic.CurrentGame.CrewRoster.Exists(crew.name)).ToList()) // Specified crew doesn't exist, WTF??? + { + crewData.Remove(crew); // Remove the invalid crew from the crewData list. + var newCrewMember = HighLogic.CurrentGame.CrewRoster.GetNewKerbal(ProtoCrewMember.KerbalType.Crew); // Try generating a new crew member and copying the expected name and gender to it. + if (newCrewMember.ChangeName(crew.name)) + { + newCrewMember.gender = crew.gender; + KerbalRoster.SetExperienceTrait(newCrewMember, KerbalRoster.pilotTrait); // Make the kerbal a pilot (so they can use SAS properly). + KerbalRoster.SetExperienceLevel(newCrewMember, KerbalRoster.GetExperienceMaxLevel()); // Make them experienced. + newCrewMember.isBadass = true; // Make them bad-ass (likes nearby explosions). + crewData.Add(newCrewMember); // Add them into the crewData list. + } + else + { + HighLogic.CurrentGame.CrewRoster.Remove(newCrewMember); + Debug.LogError($"[BDArmory.VesselSpawner]: Failed to recreate the missing crew member ({crew.name}), removing from specified crew."); + } + } + foreach (var crew in crewData) crew.rosterStatus = ProtoCrewMember.RosterStatus.Available; // Make sure the rest are available. + if (crewParts.Sum(p => p.CrewCapacity - p.protoModuleCrew.Count) < crewData.Count) Debug.LogWarning($"[BDArmory.VesselSpawner]: {crewData.Count} crew requested, but only {crewParts.Sum(p => p.CrewCapacity - p.protoModuleCrew.Count)} crew positions available. Not all requested crew will be used."); + } + int specifiedCrewUsed = 0; foreach (var part in crewParts) { - int crewToAdd = (BDArmorySettings.VESSEL_SPAWN_FILL_SEATS > 0 || (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 42)) ? part.CrewCapacity - part.protoModuleCrew.Count : 1; + int crewToAdd = (BDArmorySettings.VESSEL_SPAWN_FILL_SEATS > 0 || (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 42)) ? + part.CrewCapacity - part.protoModuleCrew.Count : crewData != null && crewData.Count - specifiedCrewUsed > 0 ? + Math.Min(crewData.Count - specifiedCrewUsed, part.CrewCapacity - part.protoModuleCrew.Count) : 1; for (int crewCount = 0; crewCount < crewToAdd; ++crewCount) { - // Create the ProtoCrewMember - ProtoCrewMember crewMember = HighLogic.CurrentGame.CrewRoster.GetNextOrNewKerbal(ProtoCrewMember.KerbalType.Crew); + ProtoCrewMember crewMember = null; + if (crewData != null && specifiedCrewUsed < crewData.Count) // Crew specified. Add them in order and fill the rest with non-reserved kerbals. + { + crewMember = crewData[specifiedCrewUsed++]; + } + if (crewMember == null) // Create the ProtoCrewMember + { + crewMember = HighLogic.CurrentGame.CrewRoster.GetNextOrNewKerbal(ProtoCrewMember.KerbalType.Crew); + while (ReservedCrew.Contains(crewMember.name)) // Skip the reserved kerbals. + { + crewMember.rosterStatus = ProtoCrewMember.RosterStatus.Assigned; // Mark them as assigned so they don't get chosen again. + reservedCrew.Add(crewMember); + crewMember = HighLogic.CurrentGame.CrewRoster.GetNextOrNewKerbal(ProtoCrewMember.KerbalType.Crew); + } + } KerbalRoster.SetExperienceTrait(crewMember, KerbalRoster.pilotTrait); // Make the kerbal a pilot (so they can use SAS properly). KerbalRoster.SetExperienceLevel(crewMember, KerbalRoster.GetExperienceMaxLevel()); // Make them experienced. crewMember.isBadass = true; // Make them bad-ass (likes nearby explosions). // Add them to the part part.AddCrewmemberAt(crewMember, part.protoModuleCrew.Count); + crewMember.rosterStatus = ProtoCrewMember.RosterStatus.Assigned; + if (BDArmorySettings.DEBUG_SPAWNING) Debug.Log($"[BDArmory.VesselSpawner]: Adding {crewMember.name} to {part.name} on {vesselData.name}"); } } + foreach (var reservedCrewMember in reservedCrew) + { + reservedCrewMember.rosterStatus = ProtoCrewMember.RosterStatus.Available; // Make the reserved crew avaiable again for the next vessel. + } // Create a dummy ProtoVessel, we will use this to dump the parts to a config node. // We can't use the config nodes from the .craft file, because they are in a @@ -243,24 +352,9 @@ static Vessel SpawnVessel(VesselData vesselData, out EditorFacility shipFacility int rootPartIndex = 0; ConfigNode protoVesselNode = ProtoVessel.CreateVesselNode(vesselData.name, vesselData.vesselType, vesselData.orbit, rootPartIndex, partNodes, additionalNodes); - // Additional settings for a landed vessel if (!vesselData.orbiting) { - // AUBRANIUM, this finding a flat spot is more appropriate to use externally to this function (e.g., in the spawn strategies) and only when spawning landed vessels. Additionally, there are potentially issues if such adjustments to the spawning location cause craft to be spawned on top of one another. - var shouldFindFlatSpot = true; - if (shouldFindFlatSpot) - { - double newLatitude, newLongitude; - if (vesselData.body.FindFlatSpotNear(vesselData.latitude, vesselData.longitude, out newLatitude, out newLongitude, 500)) - { - Debug.Log(string.Format("VesselSpawner found flat spot near {0}, {1} at {2}, {3}", vesselData.latitude, vesselData.longitude, newLatitude, newLongitude)); - vesselData.latitude = newLatitude; - vesselData.longitude = newLongitude; - } - } - Vector3d norm = vesselData.body.GetRelSurfaceNVector(vesselData.latitude, vesselData.longitude); - bool splashed = false;// = landed && terrainHeight < 0.001; // Create the config node representation of the ProtoVessel @@ -305,13 +399,8 @@ static Vessel SpawnVessel(VesselData vesselData, out EditorFacility shipFacility lowest = 0; } - // AUBRANIUM, I see that you changed some of the rotations below here. I've undone your custom changes (which break the orientation for VAB vessels) and corrected how the rotations should be for SPH and VAB (VesselMover also had them wrong). - // However, this does not account for control point orientation, which only exists once the reference transform for the vessel is loaded (not the protovessel here). - // So, the correct way to handle this is to assign the vessel's reference transform from the vessel's root part (after two fixed updates (see SpawnAllVesselsOnceCoroutine lines 702-790 and 895-901)) and then rotate it into position (and then fix things again if KSP messes with them). - // Another thing, please use the format Debug.Log("[BDArmory.]: ); when making debug log entries for ease of parsing the KSP.log file. - // Figure out the surface height and rotation - Quaternion normal = Quaternion.LookRotation((Vector3)norm);// new Vector3((float)norm.x, (float)norm.y, (float)norm.z)); + Quaternion normal = Quaternion.LookRotation((Vector3)vesselData.body.GetRelSurfaceNVector(vesselData.latitude, vesselData.longitude)); Quaternion rotation = Quaternion.identity; float heading = vesselData.heading; if (shipConstruct == null) @@ -406,7 +495,7 @@ public CrewData(CrewData cd) addToRoster = cd.addToRoster; } } - + internal class VesselData { public string name = null; diff --git a/BDArmory/VesselSpawning/VesselSpawnerBase.cs b/BDArmory/VesselSpawning/VesselSpawnerBase.cs new file mode 100644 index 000000000..96bfbba73 --- /dev/null +++ b/BDArmory/VesselSpawning/VesselSpawnerBase.cs @@ -0,0 +1,973 @@ +using UnityEngine; +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +using BDArmory.Control; +using BDArmory.Competition; +using BDArmory.Extensions; +using BDArmory.GameModes; +using BDArmory.Settings; +using BDArmory.Utils; +using BDArmory.UI; + +namespace BDArmory.VesselSpawning +{ + /// + /// Status for spawning. + /// External libraries should look for and use these. + /// + public static class VesselSpawnerStatus + { + public static bool vesselsSpawning // Flag for when vessels are being spawned and other things should wait for them to finish being spawned. + { + get { return _vesselsSpawning; } + set + { + _vesselsSpawning = value + || (CircularSpawning.Instance != null && CircularSpawning.Instance.vesselsSpawning) + || (SingleVesselSpawning.Instance != null && SingleVesselSpawning.Instance.vesselsSpawning); + } // Add in other relevant conditions whenever new classes derived from VesselSpawnerBase are added. + } + static bool _vesselsSpawning = false; + public static bool vesselSpawnSuccess // Flag for whether vessel spawning was successful or not across all derived VesselSpawner classes. + { + get { return _vesselSpawnSuccess; } + set + { + _vesselSpawnSuccess = value + && (CircularSpawning.Instance == null || CircularSpawning.Instance.vesselSpawnSuccess) + && (SingleVesselSpawning.Instance == null || SingleVesselSpawning.Instance.vesselSpawnSuccess); + } // Add in other relevant conditions whenever new classes derived from VesselSpawnerBase are added. + } + static bool _vesselSpawnSuccess = true; + public static SpawnFailureReason spawnFailureReason = SpawnFailureReason.None; + public static bool inhibitCameraTools => vesselsSpawning; // Flag for CameraTools (currently just checks for vessels being spawned). + } + + /// Base class for VesselSpawner classes so that it can work with spawn strategies. + public abstract class VesselSpawnerBase : MonoBehaviour + { + protected static string AutoSpawnPath; + protected static readonly string AutoSpawnFolder = "AutoSpawn"; + public bool vesselsSpawning { get { return _vesselsSpawning; } set { _vesselsSpawning = value; VesselSpawnerStatus.vesselsSpawning = value; } } + bool _vesselsSpawning = false; + public bool vesselSpawnSuccess { get { return _vesselSpawnSuccess; } set { _vesselSpawnSuccess = value; VesselSpawnerStatus.vesselSpawnSuccess = value; } } + bool _vesselSpawnSuccess = true; + public SpawnFailureReason spawnFailureReason { get { return VesselSpawnerStatus.spawnFailureReason; } set { VesselSpawnerStatus.spawnFailureReason = value; } } + protected static readonly WaitForFixedUpdate waitForFixedUpdate = new WaitForFixedUpdate(); + + protected virtual void Awake() + { + AutoSpawnPath = Path.GetFullPath(Path.Combine(KSPUtil.ApplicationRootPath, AutoSpawnFolder)); + } + + protected void LogMessageFrom(string derivedClassName, string message, bool toScreen, bool toLog) + { + if (toScreen) BDACompetitionMode.Instance.competitionStatus.Add(message); + if (toLog) Debug.Log($"[BDArmory.{derivedClassName}]: " + message); + } + void LogMessage(string message, bool toScreen = true, bool toLog = true) => LogMessageFrom("VesselSpawnerBase", message, toScreen, toLog); + + #region SpawnStrategy kludges + public abstract IEnumerator Spawn(SpawnConfig spawnConfig); // FIXME This is essentially a kludge to get the VesselSpawner class to be functional with the way that the SpawnStrategy interface is defined. + #endregion + + // ====================================================== + // Vessel Spawning Functions and Coroutines + // Check for "spawnFailureReason != SpawnFailureReason.None" after calling any of these coroutines to determine success/failure. + // The message will have already been displayed/logged, but you'll need to set "vesselsSpawning = false" and "yield break" on failure. + + #region Pre-spawn + public virtual void PreSpawnInitialisation(SpawnConfig spawnConfig) + { + //Reset gravity + if (BDArmorySettings.GRAVITY_HACKS) + { + PhysicsGlobals.GraviticForceMultiplier = 1d; + VehiclePhysics.Gravity.Refresh(); + } + + // If we're on another planetary body, first switch to the proper one. + if (spawnConfig.worldIndex != FlightGlobals.currentMainBody.flightGlobalsIndex) + { SpawnUtils.ShowSpawnPoint(spawnConfig.worldIndex, spawnConfig.latitude, spawnConfig.longitude, spawnConfig.altitude); } + + if (spawnConfig.killEverythingFirst) + { + BDACompetitionMode.Instance.LogResults("due to spawning", "auto-dump-from-spawning"); // Log results first. + BDACompetitionMode.Instance.StopCompetition(); // Stop any running competition. + BDACompetitionMode.Instance.ResetCompetitionStuff(); // Reset competition scores. + } + } + #endregion + + #region Early-spawn + // Common group-spawning variables. For individual craft, use local versions instead to avoid conflicts. + protected double terrainAltitude { get; set; } + protected Vector3d spawnPoint { get; set; } + + /// + /// Acquire the spawn point, killing off other vessels or check for the default 100km PRE range. + /// + /// The spawn configuration + /// The viewing distance if killing everything off and relocating the camera. + /// Whether the craft are to be air-spawned or not (also only if relocating the camera). + /// + protected IEnumerator AcquireSpawnPoint(SpawnConfig spawnConfig, float viewDistance, bool spawnAirborne) + { + if (spawnConfig.killEverythingFirst) // If we're killing everything, relocate the camera and floating origin to the spawn point and wait for the terrain. Note: this sets the variables in the "else" branch. + { + yield return SpawnUtils.RemoveAllVessels(); + yield return WaitForTerrain(spawnConfig, viewDistance, spawnAirborne); + } + else // Otherwise, just try spawning at the specified location. + { + // Get the spawning point in world position coordinates. + terrainAltitude = FlightGlobals.currentMainBody.TerrainAltitude(spawnConfig.latitude, spawnConfig.longitude); + spawnPoint = FlightGlobals.currentMainBody.GetWorldSurfacePosition(spawnConfig.latitude, spawnConfig.longitude, terrainAltitude + spawnConfig.altitude); + if ((spawnPoint - FloatingOrigin.fetch.offset).magnitude > 100e3) + { LogMessage("WARNING The spawn point is " + ((spawnPoint - FloatingOrigin.fetch.offset).magnitude / 1000).ToString("G4") + "km away. Expect vessels to be killed immediately.", true, false); } + } + } + + protected IEnumerator WaitForTerrain(SpawnConfig spawnConfig, float viewDistance, bool spawnAirborne) + { + // Update the floating origin offset, so that the vessels spawn within range of the physics. + SpawnUtils.ShowSpawnPoint(spawnConfig.worldIndex, spawnConfig.latitude, spawnConfig.longitude, spawnConfig.altitude, viewDistance, true); + // Re-acquire the spawning point after the floating origin shift. + terrainAltitude = FlightGlobals.currentMainBody.TerrainAltitude(spawnConfig.latitude, spawnConfig.longitude); + spawnPoint = FlightGlobals.currentMainBody.GetWorldSurfacePosition(spawnConfig.latitude, spawnConfig.longitude, terrainAltitude + spawnConfig.altitude); + FloatingOrigin.SetOffset(spawnPoint); // This adjusts local coordinates, such that spawnPoint is (0,0,0), which should hopefully help with collider detection. + + if (terrainAltitude > 0) // Not over the ocean or on a surfaceless body. + { + // Wait for the terrain to load in before continuing. + Ray ray; + RaycastHit hit; + var radialUnitVector = (spawnPoint - FlightGlobals.currentMainBody.transform.position).normalized; + var testPosition = spawnPoint + 1000f * radialUnitVector; + var terrainDistance = 1000f + (float)spawnConfig.altitude; + var lastTerrainDistance = terrainDistance; + var distanceToCoMainBody = (testPosition - FlightGlobals.currentMainBody.transform.position).magnitude; + ray = new Ray(testPosition, -radialUnitVector); + LogMessage("Waiting up to 10s for terrain to settle.", true, BDArmorySettings.DEBUG_SPAWNING); + var startTime = Planetarium.GetUniversalTime(); + double lastStableTimeStart = startTime; + double stableTime = 0; + do + { + lastTerrainDistance = terrainDistance; + yield return waitForFixedUpdate; + terrainDistance = Physics.Raycast(ray, out hit, (float)distanceToCoMainBody, (int)LayerMasks.Scenery) ? hit.distance : -1f; // Oceans shouldn't be more than 10km deep... + if (terrainDistance < 0f) // Raycast is failing to find terrain. + { + if (Planetarium.GetUniversalTime() - startTime < 1) continue; // Give the terrain renderer a chance to spawn the terrain. + else break; + } + if (Mathf.Abs(lastTerrainDistance - terrainDistance) > 0.1f) + lastStableTimeStart = Planetarium.GetUniversalTime(); // Reset the stable time tracker. + stableTime = Planetarium.GetUniversalTime() - lastStableTimeStart; + } while (Planetarium.GetUniversalTime() - startTime < 10 && stableTime < 1f); + if (terrainDistance < 0) + { + if (!spawnAirborne) + { + LogMessage("Failed to find terrain at the spawning point! Try increasing the spawn altitude."); + spawnFailureReason = SpawnFailureReason.NoTerrain; + yield break; + } + else + { + if (BDArmorySettings.DEBUG_SPAWNING) LogMessage("Failed to find terrain at the spawning point!"); + } + } + else + { + spawnPoint = hit.point + (float)spawnConfig.altitude * hit.normal; + } + } + } + #endregion + + #region Spawning + public int vesselsSpawningCount = 0; + protected string latestSpawnedVesselName = ""; + protected Dictionary spawnedVessels = new Dictionary(); // Vessel name => vessel instance. + protected Dictionary spawnedVesselURLs = new Dictionary(); // Vessel name => URL. + protected Dictionary spawnedVesselsTeamIndex = new Dictionary(); // Vessel name => team index + protected Dictionary spawnedVesselPartCounts = new Dictionary(); // Vessel name => part count. + protected Dictionary finalSpawnPositions = new Dictionary(); // Vessel name => final spawn position (for later reuse). + protected Dictionary finalSpawnRotations = new Dictionary(); // Vessel name => final spawn rotation (for later reuse). + protected void ResetInternals() + { + // Clear our internal collections and counters. + vesselsSpawningCount = 0; + spawnedVessels.Clear(); + spawnedVesselURLs.Clear(); + spawnedVesselsTeamIndex.Clear(); + spawnedVesselPartCounts.Clear(); + finalSpawnPositions.Clear(); + finalSpawnRotations.Clear(); + } + + protected IEnumerator SpawnVessels(List vesselSpawnConfigs) + { + ResetInternals(); + // Perform the actual spawning concurrently. + LogMessage("Spawning vessels...", false); + List spawningVessels = new List(); + foreach (var vesselSpawnConfig in vesselSpawnConfigs) + spawningVessels.Add(StartCoroutine(SpawnSingleVessel(vesselSpawnConfig))); + yield return new WaitWhile(() => vesselsSpawningCount > 0 && spawnFailureReason == SpawnFailureReason.None); + if (spawnFailureReason == SpawnFailureReason.None && spawnedVessels.Count == 0) + { + spawnFailureReason = SpawnFailureReason.VesselFailedToSpawn; + LogMessage("No vessels were spawned!"); + } + if (spawnFailureReason != SpawnFailureReason.None) + { + foreach (var cr in spawningVessels) StopCoroutine(cr); + } + } + + protected IEnumerator SpawnSingleVessel(VesselSpawnConfig vesselSpawnConfig) + { + ++vesselsSpawningCount; + + Vessel vessel; + Vector3d craftGeoCoords; + var radialUnitVector = (vesselSpawnConfig.position - FlightGlobals.currentMainBody.transform.position).normalized; + vesselSpawnConfig.position += 1000f * radialUnitVector; // Adjust the spawn point upwards by 1000m. + FlightGlobals.currentMainBody.GetLatLonAlt(vesselSpawnConfig.position, out craftGeoCoords.x, out craftGeoCoords.y, out craftGeoCoords.z); // Convert spawn point (+1000m) to geo-coords for the actual spawning function. + try + { + // Spawn the craft with zero pitch, roll and yaw as the final rotation depends on the root transform, which takes some time to be populated. + vessel = VesselSpawner.SpawnVesselFromCraftFile(vesselSpawnConfig.craftURL, craftGeoCoords, 0f, 0f, 0f, out vesselSpawnConfig.editorFacility, vesselSpawnConfig.crew); // SPAWN + } + catch (Exception e) + { + Debug.LogException(e); + vessel = null; + } + if (vessel == null) + { + var craftName = Path.GetFileNameWithoutExtension(vesselSpawnConfig.craftURL); + LogMessage("Failed to spawn craft " + craftName); + yield break; // Note: this doesn't cancel spawning. + } + else if (BDArmorySettings.DEBUG_SPAWNING) LogMessage($"Initial spawn of {vessel.vesselName} succeeded.", false); + vessel.Landed = false; // Tell KSP that it's not landed so KSP doesn't mess with its position. + if (vesselSpawnConfig.reuseURLVesselName && spawnedVesselURLs.ContainsValue(vesselSpawnConfig.craftURL)) + { + vessel.vesselName = spawnedVesselURLs.Where(kvp => kvp.Value == vesselSpawnConfig.craftURL).Select(kvp => kvp.Key).First(); + } + else + { + if (spawnedVesselURLs.ContainsKey(vessel.vesselName)) + { + var count = 1; + var potentialName = vessel.vesselName + "_" + count; + while (spawnedVesselURLs.ContainsKey(potentialName) && count < 100) + potentialName = vessel.vesselName + "_" + (++count); + if (count == 100) + { + LogMessage($"Unable to find a non-conflicting name for {vessel.vesselName}"); + spawnFailureReason = SpawnFailureReason.TimedOut; + yield break; + } + vessel.vesselName = potentialName; + } + spawnedVesselURLs.Add(vessel.vesselName, vesselSpawnConfig.craftURL); + } + var vesselName = vessel.vesselName; + latestSpawnedVesselName = vesselName; + spawnedVesselsTeamIndex[vesselName] = vesselSpawnConfig.teamIndex; // For specific team assignments. + var heightFromTerrain = vessel.GetHeightFromTerrain() - 35f; // The SpawnVesselFromCraftFile routine adds 35m for some reason. + + // Wait until the vessel's part list gets updated. + var tic = Time.time; + do + { + yield return waitForFixedUpdate; + if (vessel == null) + { + LogMessage(vesselName + " disappeared during spawning!"); + if (!BDArmorySetup.Instance.CheckDependencies()) // Check for PRE not being enabled, which can cause this. + { + LogMessage($"PRE isn't enabled!", false); + spawnFailureReason = SpawnFailureReason.DependencyIssues; + } + else spawnFailureReason = SpawnFailureReason.VesselLostParts; + yield break; + } + } while (vessel.Parts.Count == 0 && Time.time - tic < 30f); + if (vessel.Parts.Count == 0) + { + LogMessage($"Parts list on {vessel.vesselName} failed to populate within 30s."); + if (!BDArmorySetup.Instance.CheckDependencies()) // Check for PRE not being enabled, which can cause this. + { + LogMessage($"PRE isn't enabled!", false); + spawnFailureReason = SpawnFailureReason.DependencyIssues; + } + else spawnFailureReason = SpawnFailureReason.VesselFailedToSpawn; + yield break; + } + spawnedVesselPartCounts[vesselName] = SpawnUtils.PartCount(vessel); // Get the part-count without EVA kerbals. + + // Wait another update so that the reference transforms get updated. + yield return waitForFixedUpdate; + var startTime = Time.time; + // Sometimes if a vessel camera switch occurs, the craft appears unloaded for a couple of frames. This avoids NREs for control surfaces triggered by the change in reference transform. + while (vessel != null && (vessel.ReferenceTransform == null || vessel.rootPart == null || vessel.rootPart.GetReferenceTransform() == null) && (Time.time - startTime < 1f)) yield return waitForFixedUpdate; + if (vessel == null || vessel.rootPart == null) + { + LogMessage((vessel == null) ? (vesselName + " disappeared during spawning!") : (vesselName + " had no root part during spawning!")); + spawnFailureReason = SpawnFailureReason.VesselLostParts; + yield break; + } + vessel.SetReferenceTransform(vessel.rootPart); // Set the reference transform to the root part's transform. This includes setting the control point orientation. + + // Now rotate the vessel and put it at the right altitude. + var ray = new Ray(vesselSpawnConfig.position, -radialUnitVector); + RaycastHit hit; + var distanceToCoMainBody = (ray.origin - FlightGlobals.currentMainBody.transform.position).magnitude; + float distance; + Vector3 localSurfaceNormal = -ray.direction; + var localTerrainAltitude = BodyUtils.GetTerrainAltitudeAtPos(ray.origin); + if (localTerrainAltitude > 0 && Physics.Raycast(ray, out hit, distanceToCoMainBody, (int)LayerMasks.Scenery)) + { + distance = hit.distance; + localSurfaceNormal = hit.normal; + } + else + { + distance = BodyUtils.GetRadarAltitudeAtPos(ray.origin); + localSurfaceNormal = radialUnitVector; + if (BDArmorySettings.DEBUG_SPAWNING && localTerrainAltitude > 0) LogMessage("Failed to find terrain for spawn adjustments", false); + } + // Rotation + vessel.SetRotation(Quaternion.FromToRotation(vesselSpawnConfig.editorFacility == EditorFacility.SPH ? -vessel.ReferenceTransform.forward : vessel.ReferenceTransform.up, localSurfaceNormal) * vessel.transform.rotation); // Re-orient the vessel to the terrain normal (or radial unit vector). + vessel.SetRotation(Quaternion.AngleAxis(Vector3.SignedAngle(vesselSpawnConfig.editorFacility == EditorFacility.SPH ? vessel.ReferenceTransform.up : -vessel.ReferenceTransform.forward, vesselSpawnConfig.direction, localSurfaceNormal), localSurfaceNormal) * vessel.transform.rotation); // Re-orient the vessel to the right direction. + if (vesselSpawnConfig.airborne && (!BDArmorySettings.SF_GRAVITY && !BDArmorySettings.SF_REPULSOR)) + { vessel.SetRotation(Quaternion.AngleAxis(-vesselSpawnConfig.pitch, vessel.ReferenceTransform.right) * vessel.transform.rotation); } + // Position + if (FlightGlobals.currentMainBody.hasSolidSurface) + { vesselSpawnConfig.position += radialUnitVector * (vesselSpawnConfig.altitude + heightFromTerrain - distance); } + else + { vesselSpawnConfig.position -= 1000f * radialUnitVector; } + if (vessel.mainBody.ocean) // Check for being under water. + { + var distanceUnderWater = -FlightGlobals.getAltitudeAtPos(vesselSpawnConfig.position); + if (distanceUnderWater >= 0) // Under water. + { + vessel.Splashed = true; // Set the vessel as splashed. + } + } + vessel.SetPosition(vesselSpawnConfig.position); + finalSpawnPositions[vesselName] = vesselSpawnConfig.position; + finalSpawnRotations[vesselName] = vessel.transform.rotation; + vessel.altimeterDisplayState = AltimeterDisplayState.AGL; + // Fix staging (this seems to put them in the right stages, but some parts don't always work, e.g., parachutes) + vessel.currentStage = 0; + foreach (var part in vessel.parts) + { + if (part.inverseStage >= 0) part.originalStage = part.inverseStage; + vessel.currentStage = System.Math.Max(vessel.currentStage, part.originalStage + 1); + } + vessel.ResumeStaging(); // Trigger staging to resume to get staging icons to work properly. + + // Game mode adjustments. + if (BDArmorySettings.SPACE_HACKS) + { + var SF = vessel.rootPart.FindModuleImplementing(); + if (SF == null) + { + SF = (ModuleSpaceFriction)vessel.rootPart.AddModule("ModuleSpaceFriction"); + } + } + if (BDArmorySettings.MUTATOR_MODE) + { + var MM = vessel.rootPart.FindModuleImplementing(); + if (MM == null) + { + MM = (BDAMutator)vessel.rootPart.AddModule("BDAMutator"); + } + } + if (BDArmorySettings.HACK_INTAKES) + { + SpawnUtils.HackIntakes(vessel, true); + } + + LogMessage("Vessel " + vesselName + " spawned!", false); + spawnedVessels[vesselName] = vessel; + --vesselsSpawningCount; + } + #endregion + + #region Post-spawning + #region Multi-vessel post-spawn functions + /// + /// Perform the main sequence of post-spawn checks and functions for a group of vessels. + /// + /// + /// + /// + protected IEnumerator PostSpawnMainSequence(SpawnConfig spawnConfig, bool spawnAirborne, bool ignoreValidity = false) + { + if (!ignoreValidity) + { + if (BDArmorySettings.DEBUG_SPAWNING) LogMessage("Checking vessel validity", false); + yield return CheckVesselValidity(spawnedVessels); + if (spawnFailureReason != SpawnFailureReason.None) yield break; + + if (BDArmorySettings.DEBUG_SPAWNING) LogMessage("Waiting for weapon managers", false); + yield return WaitForWeaponManagers(spawnedVessels, spawnedVesselPartCounts, spawnConfig.numberOfTeams != 1 && spawnConfig.numberOfTeams != -1); + if (spawnFailureReason != SpawnFailureReason.None) yield break; + } + else + { + yield return waitForFixedUpdate; // We need to yield two frames for the spawned vessels' positions to be updated properly. + yield return waitForFixedUpdate; + } + + // Reset craft positions and rotations as sometimes KSP packs and unpacks vessels between frames and resets things! (Possibly due to kerbals in command seats?) + if (BDArmorySettings.DEBUG_SPAWNING) LogMessage("Resetting final spawn positions", false); + ResetFinalSpawnPositionsAndRotations(spawnedVessels, finalSpawnPositions, finalSpawnRotations); + + // Lower vessels to the ground or activate them in the air. + if (spawnConfig.altitude >= 0 && !spawnAirborne) + { + if (BDArmorySettings.DEBUG_SPAWNING) LogMessage("Lowering vessels", false); + yield return PlaceSpawnedVessels(spawnedVessels.Values.ToList()); + if (spawnFailureReason != SpawnFailureReason.None) yield break; + + // Check that none of the vessels have lost parts. + if (spawnedVessels.Any(kvp => SpawnUtils.PartCount(kvp.Value) < spawnedVesselPartCounts[kvp.Key])) + { + var offendingVessels = spawnedVessels.Where(kvp => SpawnUtils.PartCount(kvp.Value) < spawnedVesselPartCounts[kvp.Key]); + LogMessage("Part-count of some vessels changed after spawning: " + string.Join(", ", offendingVessels.Select(kvp => kvp.Value == null ? "null" : kvp.Value.vesselName + $" ({spawnedVesselPartCounts[kvp.Key] - SpawnUtils.PartCount(kvp.Value)})"))); + spawnFailureReason = SpawnFailureReason.VesselLostParts; + yield break; + } + } + else + { + if (BDArmorySettings.DEBUG_SPAWNING) LogMessage("Activating vessels in the air", false); + AirborneActivation(spawnedVessels); + } + if (spawnFailureReason != SpawnFailureReason.None) yield break; + + // One last check for renamed vessels (since we're not entirely sure when this occurs). + if (BDArmorySettings.DEBUG_SPAWNING) LogMessage("Checking for renamed vessels", false); + SpawnUtils.CheckForRenamedVessels(spawnedVessels); + + if (BDArmorySettings.RUNWAY_PROJECT && !ignoreValidity) + { + // Check AI/WM counts and placement for RWP. + foreach (var vesselName in spawnedVessels.Keys) + { + SpawnUtils.CheckAIWMCounts(spawnedVessels[vesselName]); + SpawnUtils.CheckAIWMPlacement(spawnedVessels[vesselName]); + } + } + } + + /// + /// Check a group of vessels for being valid. Times out after 1s. + /// + /// + /// + protected IEnumerator CheckVesselValidity(Dictionary vessels) + { + var startTime = Time.time; + Dictionary invalidVessels; + // Check that the spawned vessels are valid craft + do + { + yield return waitForFixedUpdate; + invalidVessels = vessels.ToDictionary(kvp => kvp.Key, kvp => BDACompetitionMode.Instance.IsValidVessel(kvp.Value)).Where(kvp => kvp.Value != BDACompetitionMode.InvalidVesselReason.None).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + } while (invalidVessels.Count > 0 && Time.time - startTime < 1); // Give it up to 1s for KSP to populate the vessel's AI and WM. + if (invalidVessels.Count > 0) + { + LogMessage("The following vessels are invalid:\n - " + string.Join("\n - ", invalidVessels.Select(t => t.Key + " : " + t.Value)), true, false); + LogMessage("Invalid vessels: " + string.Join(", ", invalidVessels.Select(t => t.Key + ":" + t.Value)), false, true); + spawnFailureReason = SpawnFailureReason.InvalidVessel; + } + } + + /// + /// Wait for weapon managers of a group of vessels to appear in the Vessel Switcher. + /// + /// + /// + /// + /// + protected IEnumerator WaitForWeaponManagers(Dictionary vessels, Dictionary vesselPartCounts, bool saveTeams) + { + var vesselsToCheck = vessels.Keys.ToList(); + var allWeaponManagersAssigned = false; + var startTime = Time.time; + do + { + yield return waitForFixedUpdate; + SpawnUtils.CheckForRenamedVessels(vessels); + + // Check that none of the vessels have lost parts. + if (vessels.Any(kvp => kvp.Value == null || SpawnUtils.PartCount(kvp.Value) < vesselPartCounts[kvp.Key])) + { + var offendingVessels = vessels.Where(kvp => kvp.Value == null || SpawnUtils.PartCount(kvp.Value) < vesselPartCounts[kvp.Key]); + LogMessage("Part-count of some vessels changed after spawning: " + string.Join(", ", offendingVessels.Select(kvp => kvp.Value == null ? "null" : kvp.Value.vesselName + $" ({vesselPartCounts[kvp.Key] - SpawnUtils.PartCount(kvp.Value)})"))); + spawnFailureReason = SpawnFailureReason.VesselLostParts; + yield break; + } + + // Wait for all the weapon managers to be added to LoadedVesselSwitcher. + LoadedVesselSwitcher.Instance.UpdateList(); + var weaponManagers = LoadedVesselSwitcher.Instance.WeaponManagers.SelectMany(tm => tm.Value).ToList(); + foreach (var vesselName in vesselsToCheck.ToList()) + { + var weaponManager = VesselModuleRegistry.GetModule(vessels[vesselName]); + if (weaponManager != null && weaponManagers.Contains(weaponManager)) // The weapon manager has been added, let's go! + { vesselsToCheck.Remove(vesselName); } + } + if (vesselsToCheck.Count == 0) + allWeaponManagersAssigned = true; + + if (allWeaponManagersAssigned) + { + if (saveTeams) // Already assigned. + SpawnUtils.SaveTeams(); + yield break; // Success! + } + } while (Time.time - startTime < 10); // Give it up to 10s for the weapon managers to get added to the LoadedVesselSwitcher's list. + LogMessage("Timed out waiting for weapon managers to appear in the Vessel Switcher.", true, false); + spawnFailureReason = SpawnFailureReason.TimedOut; + } + + protected void ResetFinalSpawnPositionsAndRotations(Dictionary vessels, Dictionary positions, Dictionary rotations) + { + // Reset craft positions and rotations as sometimes KSP packs and unpacks vessels between frames and resets things! + foreach (var vesselName in vessels.Keys) + { + if (vessels[vesselName] == null) continue; + vessels[vesselName].SetPosition(finalSpawnPositions[vesselName]); + vessels[vesselName].SetRotation(finalSpawnRotations[vesselName]); + } + } + + /// + /// [Deprecated] Use PlaceSpawnedVessels instead. + /// + /// + /// + /// + /// + /// + [Obsolete("LowerVesselsToSurface is deprecated, please use PlaceSpawnedVessels instead.")] + protected IEnumerator LowerVesselsToSurface(Dictionary vessels, Dictionary partCounts, float easeInSpeed, double altitude) + { + var radialUnitVectors = vessels.ToDictionary(v => v.Key, v => (v.Value.transform.position - FlightGlobals.currentMainBody.transform.position).normalized); + // Prevent the vessels from falling too fast and check if their velocities in the surface normal direction is below a threshold. + var vesselsHaveLanded = vessels.Keys.ToDictionary(v => v, v => (int)0); // 1=started moving, 2=landed. + var landingStartTime = Time.time; + do + { + yield return waitForFixedUpdate; + foreach (var vesselName in vessels.Keys) + { + var vessel = vessels[vesselName]; + if (vessel.LandedOrSplashed && BodyUtils.GetRadarAltitudeAtPos(vessel.transform.position) <= 0) // Wait for the vessel to settle a bit in the water. The 15s buffer should be more than sufficient. + { + vesselsHaveLanded[vesselName] = 2; + } + if (vesselsHaveLanded[vesselName] == 0 && Vector3.Dot(vessel.srf_velocity, radialUnitVectors[vesselName]) < 0) // Check that vessel has started moving. + vesselsHaveLanded[vesselName] = 1; + if (vesselsHaveLanded[vesselName] == 1 && Vector3.Dot(vessel.srf_velocity, radialUnitVectors[vesselName]) >= 0) // Check if the vessel has landed. + { + vesselsHaveLanded[vesselName] = 2; + if (BodyUtils.GetRadarAltitudeAtPos(vessel.transform.position) > 0) + vessel.Landed = true; // Tell KSP that the vessel is landed. + else + vessel.Splashed = true; // Tell KSP that the vessel is splashed. + } + if (vesselsHaveLanded[vesselName] == 1 && vessel.srf_velocity.sqrMagnitude > easeInSpeed) // While the vessel hasn't landed, prevent it from moving too fast. + vessel.SetWorldVelocity(0.99 * easeInSpeed * vessel.srf_velocity); // Move at easeInSpeed m/s at most. + } + + // Check that none of the vessels have lost parts. + if (vessels.Any(kvp => SpawnUtils.PartCount(kvp.Value) < partCounts[kvp.Key])) + { + var offendingVessels = vessels.Where(kvp => SpawnUtils.PartCount(kvp.Value) < partCounts[kvp.Key]); + LogMessage("Part-count of some vessels changed after spawning: " + string.Join(", ", offendingVessels.Select(kvp => kvp.Value == null ? "null" : kvp.Value.vesselName + $" ({partCounts[kvp.Key] - SpawnUtils.PartCount(kvp.Value)})"))); + spawnFailureReason = SpawnFailureReason.VesselLostParts; + yield break; + } + + if (vesselsHaveLanded.Values.All(v => v == 2)) yield break; + } while (Time.time - landingStartTime < 15 + altitude / easeInSpeed); // Give the vessels up to (15 + altitude / easeInSpeed) seconds to land. + LogMessage("Timed out waiting for the vessels to land.", true, false); + spawnFailureReason = SpawnFailureReason.TimedOut; + } + + /// + /// Activation sequence for airborne vessels. + /// + /// + protected void AirborneActivation(Dictionary vessels) + { + foreach (var vessel in vessels.Select(v => v.Value)) + { AirborneActivation(vessel); } + } + #endregion + + #region Single vessel post-spawn functions + /// + /// Get the vessel corresponding to the craftURL from the spawnedVesselURLs dictionary. + /// Note: this is only valid when craftURLs are unique for each spawned vessel (i.e., when vesselSpawnConfig.reuseURLVesselName is true) (like in continuous spawning). + /// + /// + /// + protected Vessel GetSpawnedVesselsName(string craftURL) + { + // Find the vesselName for the craft URL. + var vesselName = spawnedVesselURLs.Where(kvp => kvp.Value == craftURL).Select(kvp => kvp.Key).FirstOrDefault(); + if (string.IsNullOrEmpty(vesselName) || !spawnedVessels.ContainsKey(vesselName)) + { + spawnFailureReason = SpawnFailureReason.VesselFailedToSpawn; + if (!string.IsNullOrEmpty(vesselName)) + { + foreach (var vessl in FlightGlobals.Vessels) // If the vessel was partially spawned, find and remove it. + { + if (vessl == null) continue; + if (vessl.vesselName == vesselName) RemoveVessel(vessl); + } + return null; + } + } + return spawnedVessels[vesselName]; + } + + /// + /// Perform the main sequence of post-spawn checks and functions for a vessel. + /// + /// + /// + /// + protected IEnumerator PostSpawnMainSequence(Vessel vessel, bool spawnAirborne) + { + var vesselName = vessel.vesselName; + + yield return CheckVesselValidity(vessel); + if (spawnFailureReason != SpawnFailureReason.None) + { + if (vessel != null) RemoveVessel(vessel); + yield break; + } + + yield return WaitForWeaponManager(vessel); + if (spawnFailureReason != SpawnFailureReason.None) + { + if (vessel != null) RemoveVessel(vessel); + yield break; + } + + // Reset craft positions and rotations as sometimes KSP packs and unpacks vessels between frames and resets things! (Possibly due to kerbals in command seats?) + vessel.SetPosition(finalSpawnPositions[vesselName]); + vessel.SetRotation(finalSpawnRotations[vesselName]); + + // Undo any camera adjustment and reset the camera distance. This has an internal check so that it only occurs once. + SpawnUtils.RevertSpawnLocationCamera(true); + if (FlightGlobals.ActiveVessel == null || FlightGlobals.ActiveVessel.state == Vessel.State.DEAD) + { + LoadedVesselSwitcher.Instance.ForceSwitchVessel(vessel); // Update the camera. + FlightCamera.fetch.SetDistance(50); + } + + // Lower vessel to the ground or activate them in the air. + if (vessel.radarAltitude >= 0 && !spawnAirborne) + { + vessel.ActionGroups.SetGroup(KSPActionGroup.Brakes, false); // Disable them first to make sure they trigger on toggling. + vessel.ActionGroups.SetGroup(KSPActionGroup.Brakes, true); + var partCount = SpawnUtils.PartCount(vessel); + yield return PlaceSpawnedVessel(vessel); + if (SpawnUtils.PartCount(vessel) != partCount) + { + LogMessage($"Part-count of {vesselName} changed after spawning: {partCount - SpawnUtils.PartCount(vessel)}"); + spawnFailureReason = SpawnFailureReason.VesselLostParts; + if (vessel != null) RemoveVessel(vessel); + yield break; + } + } + else AirborneActivation(vessel); + + // Check for the vessel having been renamed from the VESSELNAMING tag (not sure when this occurs, but it should be before now). + if (vesselName != vessel.vesselName) + vessel.vesselName = vesselName; + + if (BDArmorySettings.RUNWAY_PROJECT) + { + // Check AI/WM counts and placement for RWP. + SpawnUtils.CheckAIWMCounts(vessel); + SpawnUtils.CheckAIWMPlacement(vessel); + } + } + + /// + /// Check a single vessel for being valid. Times out after 1s. + /// + /// + /// + protected IEnumerator CheckVesselValidity(Vessel vessel) + { + var startTime = Time.time; + var validity = BDACompetitionMode.Instance.IsValidVessel(vessel); + while (validity != BDACompetitionMode.InvalidVesselReason.None && Time.time - startTime < 1) + { + yield return waitForFixedUpdate; + validity = BDACompetitionMode.Instance.IsValidVessel(vessel); + vessel.SetPosition(finalSpawnPositions[vessel.vesselName]); // Prevent the vessel from falling. + } + if (validity != BDACompetitionMode.InvalidVesselReason.None) + { + LogMessage($"The vessel {vessel.vesselName} is invalid: {validity}"); + spawnFailureReason = SpawnFailureReason.InvalidVessel; + } + } + + /// + /// Wait for the weapon manager to appear in the Vessel Switcher (single vessel version). + /// + /// + /// + protected IEnumerator WaitForWeaponManager(Vessel vessel) + { + yield return waitForFixedUpdate; // Wait at least one update so the vessel parts list is populated. + var startTime = Time.time; + var weaponManager = VesselModuleRegistry.GetModule(vessel); + var vesselName = vessel.vesselName; // In case it disappears. + var assigned = weaponManager != null && LoadedVesselSwitcher.Instance.WeaponManagers.SelectMany(tm => tm.Value).Contains(weaponManager); + while (!assigned && Time.time - startTime < 10 && vessel != null) + { + yield return waitForFixedUpdate; + if (vessel == null || SpawnUtils.PartCount(vessel) != spawnedVesselPartCounts[vesselName]) + { + LogMessage($"Part-count of {vesselName} changed after spawning: {(vessel == null ? spawnedVesselPartCounts[vesselName] : spawnedVesselPartCounts[vesselName] - SpawnUtils.PartCount(vessel))}"); + spawnFailureReason = SpawnFailureReason.VesselLostParts; + yield break; + } + if (weaponManager == null) weaponManager = VesselModuleRegistry.GetModule(vessel); + assigned = weaponManager != null && LoadedVesselSwitcher.Instance.WeaponManagers.SelectMany(tm => tm.Value).Contains(weaponManager); + vessel.SetPosition(finalSpawnPositions[vesselName]); // Prevent the vessel from falling. + } + if (!assigned) + { + LogMessage("Timed out waiting for weapon managers to appear in the Vessel Switcher.", true, false); + spawnFailureReason = SpawnFailureReason.TimedOut; + } + } + + /// + /// Activation sequence for an airborne vessel. + /// + /// Checks for the vessel or weapon manager being null or having lost parts should have been done before calling this. + /// + /// + protected void AirborneActivation(Vessel vessel) + { + // Activate the vessel with AG10, or failing that, staging. + vessel.ActionGroups.ToggleGroup(BDACompetitionMode.KM_dictAG[10]); // Modular Missiles use lower AGs (1-3) for staging, use a high AG number to not affect them + var weaponManager = VesselModuleRegistry.GetModule(vessel); + if (weaponManager != null) + { + if (weaponManager.AI != null) + { + weaponManager.AI.ActivatePilot(); + weaponManager.AI.CommandTakeOff(); + } + if (weaponManager.guardMode) + { + if (BDArmorySettings.DEBUG_SPAWNING) LogMessage($"Disabling guardMode on {vessel.vesselName}.", false); + weaponManager.ToggleGuardMode(); // Disable guard mode (in case someone enabled it on AG10 or in the SPH). + weaponManager.SetTarget(null); + } + } + + if (!BDArmorySettings.NO_ENGINES && SpawnUtils.CountActiveEngines(vessel) == 0) // If the vessel didn't activate their engines on AG10, then activate all their engines and hope for the best. + { + if (BDArmorySettings.DEBUG_SPAWNING) LogMessage(vessel.vesselName + " didn't activate engines on AG10! Activating ALL their engines.", false); + SpawnUtils.ActivateAllEngines(vessel); + } + else if (BDArmorySettings.NO_ENGINES && SpawnUtils.CountActiveEngines(vessel) > 0) // Vessel had some active engines. Turn them off if possible. + { + SpawnUtils.ActivateAllEngines(vessel, false); + } + } + + /// + /// [Deprecated] Place a spawned vessel on the ground/water surface in a single step. + /// + /// + /// Vertical offset to place the vessel. + /// The vertical distance to the lowest point on the vessel. + [Obsolete("PlaceSpawnedVessel_Old is deprecated, please use PlaceSpawnedVessels instead.")] + protected void PlaceSpawnedVessel_Old(Vessel vessel, float offset = 0, bool allowBelowWater = false) + { + if (!vessel.mainBody.hasSolidSurface) return; // Nowhere to place it! + var down = (vessel.mainBody.transform.position - vessel.CoM).normalized; + if (!allowBelowWater && BodyUtils.GetTerrainAltitudeAtPos(vessel.CoM, true) < 0) // Over water. + { + if (BDArmorySettings.DEBUG_SPAWNING) LogMessage($"{vessel.vesselName} is {vessel.altitude:G6}m above water, lowering.", false); + vessel.SetPosition(vessel.CoM + ((float)vessel.altitude - offset - 0.1f) * down); + return; + } + // Over land. + var altitude = (float)(vessel.altitude - vessel.mainBody.TerrainAltitude(vessel.latitude, vessel.longitude, allowBelowWater)); + var radius = vessel.GetRadius(down, vessel.GetBounds()); + var belowHits = Physics.SphereCastAll(vessel.CoM - (radius + 100f) * down, radius, down, altitude + 2f * radius + 100f, (int)(LayerMasks.Scenery | LayerMasks.Parts | LayerMasks.Wheels | LayerMasks.EVA)); // Start "radius" above the CoM so the minimum distance is the altitude of the CoM, +100m for safety when near other objects. + var minDistance = altitude + 2f * radius + 100f; + foreach (var belowHit in belowHits) + { + var belowHitPart = belowHit.collider.gameObject.GetComponentInParent(); + if (belowHitPart != null && belowHitPart.vessel == vessel) continue; + var hits = Physics.BoxCastAll(belowHit.point + 2.1f * down, new Vector3(radius, 0.1f, radius), -down, Quaternion.FromToRotation(Vector3.up, belowHit.normal), belowHit.distance + 3f, (int)(LayerMasks.Parts | LayerMasks.EVA | LayerMasks.Wheels)); // Start 2m below the hit to catch wheels protruding into the ground (the largest Squad wheel has radius 1m). + foreach (var hit in hits) + { + var hitPart = hit.collider.gameObject.GetComponentInParent(); + if (hitPart == null || hitPart.vessel != vessel) continue; + var distance = hit.distance - 2f; // Correct for the initial offset. + if (BDArmorySettings.DEBUG_SPAWNING) LogMessage($"{vessel.vesselName}: Distance from {belowHit.collider.name}{(belowHitPart != null ? belowHitPart.name : "")} to {hit.collider.name} ({hitPart.name}): {distance:G6}m", false); + if (distance < minDistance) + { + minDistance = distance; + } + } + } + if (BDArmorySettings.DEBUG_SPAWNING) LogMessage($"{vessel.vesselName} is {minDistance:G6}m above land, lowering.", false); + if (minDistance - offset > 0.1f) + vessel.SetPosition(vessel.transform.position + down * (minDistance - offset - 0.1f)); // Minor adjustment to prevent potential clipping. + } + + /// + /// Lower the vessels to terrain. + /// This uses the VesselMover routines. + /// + /// The list of vessels to lower. + protected IEnumerator PlaceSpawnedVessels(List vessels) + { + loweringVesselsCount = 0; // Reset the counter for good measure. + foreach (var vessel in vessels) + StartCoroutine(PlaceSpawnedVessel(vessel)); + var tic = Time.time; + yield return new WaitWhileFixed(() => loweringVesselsCount > 0 && Time.time - tic < 30); // Wait up to 30s for lowering to complete (it shouldn't take anywhere near this long!). + if (loweringVesselsCount > 0) + { + LogMessage("Timed out waiting for the vessels to land.", true, false); + spawnFailureReason = SpawnFailureReason.TimedOut; + } + } + + int loweringVesselsCount = 0; + /// + /// Lower the vessel to the terrain once it's finished loading in. + /// + /// The vessel to lower. + protected IEnumerator PlaceSpawnedVessel(Vessel vessel) + { + ++loweringVesselsCount; + var tic = Time.time; + yield return new WaitWhile(() => vessel != null && !VesselMover.Instance.IsValid(vessel) && Time.time - tic < 10); // Wait up to 10s for the vessel to finish loading in. + if (!VesselMover.Instance.IsValid(vessel)) + { + --loweringVesselsCount; + yield break; + } + if (BDArmorySettings.VESSEL_MOVER_ENABLE_SAS) vessel.ActionGroups.SetGroup(KSPActionGroup.SAS, false); // Disable SAS. These get re-enabled once the vessel is lowered. + if (BDArmorySettings.VESSEL_MOVER_ENABLE_BRAKES) vessel.ActionGroups.SetGroup(KSPActionGroup.Brakes, false); // Disable Brakes + yield return VesselMover.Instance.PlaceVessel(vessel, true); + --loweringVesselsCount; + } + + public void AddToActiveCompetition(Vessel vessel, bool airborne) + { + var vesselName = vessel.vesselName; + // If a competition is active, update the scoring structure. + if ((BDACompetitionMode.Instance.competitionStarting || BDACompetitionMode.Instance.competitionIsActive) && !BDACompetitionMode.Instance.Scores.Players.Contains(vesselName)) + { + BDACompetitionMode.Instance.Scores.AddPlayer(vessel); + } + if (ContinuousSpawning.Instance.vesselsSpawningContinuously) + { + if (!ContinuousSpawning.Instance.continuousSpawningScores.ContainsKey(vesselName)) + ContinuousSpawning.Instance.continuousSpawningScores.Add(vesselName, new ContinuousSpawning.ContinuousSpawningScores()); + ContinuousSpawning.Instance.continuousSpawningScores[vesselName].vessel = vessel; // Update some values in the scoring structure. + ContinuousSpawning.Instance.continuousSpawningScores[vesselName].outOfAmmoTime = 0; + } + + var weaponManager = VesselModuleRegistry.GetModule(vessel); + if (BDArmorySettings.TAG_MODE && !string.IsNullOrEmpty(BDACompetitionMode.Instance.Scores.currentlyIT)) + { weaponManager.SetTeam(BDTeam.Get("NO")); } + else + { + // Assign the vessel to an unassigned team. + var weaponManagers = LoadedVesselSwitcher.Instance.WeaponManagers.SelectMany(tm => tm.Value).ToList(); + var currentTeams = weaponManagers.Where(wm => wm != weaponManager).Select(wm => wm.Team).ToHashSet(); // Current teams, excluding us. + char team = 'A'; + while (currentTeams.Contains(BDTeam.Get(team.ToString()))) + ++team; + weaponManager.SetTeam(BDTeam.Get(team.ToString())); + } + + if (!airborne) + { + weaponManager.AI.ActivatePilot(); + weaponManager.AI.CommandTakeOff(); + if (!BDArmorySettings.NO_ENGINES && SpawnUtils.CountActiveEngines(vessel) == 0) // If the vessel didn't activate their engines on AG10, then activate all their engines and hope for the best. + { + if (BDArmorySettings.DEBUG_SPAWNING) LogMessage(vessel.vesselName + " didn't activate engines on AG10! Activating ALL their engines.", false); + SpawnUtils.ActivateAllEngines(vessel); + } + else if (BDArmorySettings.NO_ENGINES && SpawnUtils.CountActiveEngines(vessel) > 0) // Vessel had some active engines. Turn them off if possible. + { + SpawnUtils.ActivateAllEngines(vessel, false); + } + } + // Enable guard mode if a competition is active. + if (BDACompetitionMode.Instance.competitionIsActive && !weaponManager.guardMode) weaponManager.ToggleGuardMode(); + weaponManager.AI.ReleaseCommand(); + weaponManager.ForceScan(); + + if (ContinuousSpawning.Instance.vesselsSpawningContinuously) + { + // Adjust BDACompetitionMode's scoring structures. + ContinuousSpawning.Instance.UpdateCompetitionScores(vessel, true); + ++ContinuousSpawning.Instance.continuousSpawningScores[vesselName].spawnCount; + } + + // Update the ramming information for the new vessel. + if (BDACompetitionMode.Instance.rammingInformation != null) + { BDACompetitionMode.Instance.AddPlayerToRammingInformation(vessel); } + } + #endregion + + #region Utils + /// + /// Remove a vessel, removing it from the spawning dictionaries too. + /// + /// + protected void RemoveVessel(Vessel vessel) + { + var vesselName = vessel.vesselName; + if (spawnedVessels.ContainsKey(vesselName)) spawnedVessels.Remove(vesselName); + if (spawnedVesselURLs.ContainsKey(vesselName)) spawnedVesselURLs.Remove(vesselName); + if (spawnedVesselsTeamIndex.ContainsKey(vesselName)) spawnedVesselsTeamIndex.Remove(vesselName); + if (spawnedVesselPartCounts.ContainsKey(vesselName)) spawnedVesselPartCounts.Remove(vesselName); + if (finalSpawnPositions.ContainsKey(vesselName)) finalSpawnPositions.Remove(vesselName); + if (finalSpawnRotations.ContainsKey(vesselName)) finalSpawnRotations.Remove(vesselName); + SpawnUtils.RemoveVessel(vessel); + } + #endregion + #endregion + } +} \ No newline at end of file diff --git a/BDArmory/Competition/VesselSpawning/_description b/BDArmory/VesselSpawning/_description similarity index 100% rename from BDArmory/Competition/VesselSpawning/_description rename to BDArmory/VesselSpawning/_description diff --git a/BDArmory/Modules/BDAdjustableRail.cs b/BDArmory/WeaponMounts/BDAdjustableRail.cs similarity index 98% rename from BDArmory/Modules/BDAdjustableRail.cs rename to BDArmory/WeaponMounts/BDAdjustableRail.cs index 2539c539c..522b6d217 100644 --- a/BDArmory/Modules/BDAdjustableRail.cs +++ b/BDArmory/WeaponMounts/BDAdjustableRail.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using UnityEngine; -namespace BDArmory.Modules +namespace BDArmory.WeaponMounts { public class BDAdjustableRail : PartModule { @@ -47,7 +47,7 @@ void ParseStackNodePosition() IEnumerator DelayedUpdateStackNode() { - yield return null; + yield return new WaitForFixedUpdate(); UpdateStackNode(false); } diff --git a/BDArmory/Modules/BDDeployableRail.cs b/BDArmory/WeaponMounts/BDDeployableRail.cs similarity index 80% rename from BDArmory/Modules/BDDeployableRail.cs rename to BDArmory/WeaponMounts/BDDeployableRail.cs index 59423fb33..827df9a33 100644 --- a/BDArmory/Modules/BDDeployableRail.cs +++ b/BDArmory/WeaponMounts/BDDeployableRail.cs @@ -2,9 +2,12 @@ using System.Collections.Generic; using UniLinq; using UnityEngine; -using BDArmory.Misc; -namespace BDArmory.Modules +using BDArmory.Control; +using BDArmory.Utils; +using BDArmory.Weapons.Missiles; + +namespace BDArmory.WeaponMounts { public class BDDeployableRail : PartModule { @@ -58,6 +61,9 @@ public MissileFire weaponManager } } + [KSPAction("Toggle deployment")] + public void AGToggleRail(KSPActionParam param) => ToggleRail(); + [KSPEvent(guiActive = true, guiActiveEditor = true, guiName = "toggle deployment")]//FIXME - localize later-- public void ToggleRail() { @@ -71,7 +77,7 @@ public override void OnStart(StartState state) part.force_activate(); setupComplete = false; deployTransform = part.FindModelTransform(deployTransformName); - deployState = Utils.SetUpSingleAnimation(deployAnimName, part); + deployState = GUIUtils.SetUpSingleAnimation(deployAnimName, part); deployState.enabled = true; deployState.speed = 0; @@ -117,7 +123,7 @@ public void DeployRail(bool externallycalled) } public IEnumerator OnStartDeploy() { - yield return new WaitForSeconds(1); //figure out what the wait interval needs to be. Too soon, and the offsets get messed up. maybe have the RCS snapshot delay instead? + yield return new WaitForSecondsFixed(1); //figure out what the wait interval needs to be. Too soon, and the offsets get messed up. maybe have the RCS snapshot delay instead? UpdateMissileChildren(); DeployRail(true); } @@ -132,7 +138,7 @@ public IEnumerator Deploy() missilePart.ShieldedFromAirstream = false; if (hideMissiles) missilePart.SetOpacity(1); } - while (deployState.normalizedTime < 1) + while (deployState.normalizedTime < 1) { UpdateChildrenPos(); yield return new WaitForFixedUpdate(); @@ -143,7 +149,7 @@ public IEnumerator Deploy() deployed = true; if (HighLogic.LoadedSceneIsFlight) { - yield return new WaitForSeconds(rotationDelay); + yield return new WaitForSecondsFixed(rotationDelay); rdyToFire = true; } if (HighLogic.LoadedSceneIsEditor) @@ -190,7 +196,7 @@ public IEnumerator Retract() setupComplete = true; for (int i = 0; i < missileChildren.Length; i++) { - if (!missileTransforms[i] || !missileChildren[i] || missileChildren[i].HasFired) continue; + if (!missileTransforms[i] || !missileChildren[i]) continue; Part missilePart = missileChildren[i].part; missilePart.ShieldedFromAirstream = true; if (hideMissiles) missilePart.SetOpacity(0); @@ -215,7 +221,7 @@ public void EnableRail() if (!setupComplete) return; if (deployed) return; - Debug.Log("[MissileFire] deploying deployableRail"); + //Debug.Log("[BDArmory.BDDeployableRail]: deploying deployableRail"); StopRoutines(); deployRoutine = StartCoroutine(Deploy()); @@ -225,13 +231,13 @@ public void DisableRail() if (!setupComplete) return; if (!deployed) return; - Debug.Log("[MissileFire] retracting deployableRail"); //this is getting called when missile selected, sometihng is tripping the if statement. investigate. + //Debug.Log("[BDArmory.BDDeployableRail]: retracting deployableRail"); //this is getting called when missile selected, sometihng is tripping the if statement. investigate. StopRoutines(); if (part.isActiveAndEnabled) retractRoutine = StartCoroutine(Retract()); } - void UpdateChildrenPos() + public void UpdateChildrenPos() { /* using (List.Enumerator p = part.children.GetEnumerator()) @@ -251,7 +257,7 @@ void UpdateChildrenPos() for (int i = 0; i < missileChildren.Length; i++) { - if (!missileTransforms[i] || !missileChildren[i] || missileChildren[i].HasFired) continue; + if (!missileTransforms[i] || !missileChildren[i] || missileChildren[i].vessel != this.vessel) continue; missileTransforms[i].position = missileReferenceTransforms[i].position; //wait, is this just moving the mesh, but the part stays where it is? Would explain the need for CoM offset missileTransforms[i].rotation = missileReferenceTransforms[i].rotation; //have this reset on spawn? //float scaleVector = Mathf.Lerp(scaleVector, 1, 0.02f / deployState.length); //have the missile scale (so big missiles can fit inside shallow bays without clipping through base of the bay? Future SI, play around with this @@ -287,61 +293,61 @@ public void UpdateMissileChildren() List mtfl = new List(); List mrl = new List(); using (List.Enumerator child = part.children.GetEnumerator()) - while (child.MoveNext()) - { - if (child.Current == null) continue; - if (child.Current.parent != part) continue; + while (child.MoveNext()) + { + if (child.Current == null) continue; + if (child.Current.parent != part) continue; - MissileLauncher ml = child.Current.FindModuleImplementing(); + MissileLauncher ml = child.Current.FindModuleImplementing(); - if (!ml) continue; + if (!ml) continue; - Transform mTf = child.Current.FindModelTransform("missileTransform"); + Transform mTf = child.Current.FindModelTransform("missileTransform"); //mTf = child.Current.partTransform; - //fix incorrect hierarchy - if (!mTf) - { - Transform modelTransform = ml.part.partTransform.Find("model"); - - mTf = new GameObject("missileTransform").transform; - Transform[] tfchildren = new Transform[modelTransform.childCount]; - for (int i = 0; i < modelTransform.childCount; i++) + //fix incorrect hierarchy + if (!mTf) { - tfchildren[i] = modelTransform.GetChild(i); - } - mTf.parent = modelTransform; - mTf.localPosition = Vector3.zero; - mTf.localRotation = Quaternion.identity; - mTf.localScale = Vector3.one; - using (IEnumerator t = tfchildren.AsEnumerable().GetEnumerator()) - while (t.MoveNext()) + Transform modelTransform = ml.part.partTransform.Find("model"); + + mTf = new GameObject("missileTransform").transform; + Transform[] tfchildren = new Transform[modelTransform.childCount]; + for (int i = 0; i < modelTransform.childCount; i++) { - if (t.Current == null) continue; - t.Current.parent = mTf; + tfchildren[i] = modelTransform.GetChild(i); } - } + mTf.parent = modelTransform; + mTf.localPosition = Vector3.zero; + mTf.localRotation = Quaternion.identity; + mTf.localScale = Vector3.one; + using (IEnumerator t = tfchildren.AsEnumerable().GetEnumerator()) + while (t.MoveNext()) + { + if (t.Current == null) continue; + t.Current.parent = mTf; + } + } - if (!ml || !mTf) continue; - msl.Add(ml); - mtfl.Add(mTf); - Transform mRef = new GameObject().transform; - mRef.position = mTf.position; - mRef.rotation = mTf.rotation; - mRef.parent = deployTransform; - mrl.Add(mRef); + if (!ml || !mTf) continue; + msl.Add(ml); + mtfl.Add(mTf); + Transform mRef = new GameObject().transform; + mRef.position = mTf.position; + mRef.rotation = mTf.rotation; + mRef.parent = deployTransform; + mrl.Add(mRef); - ml.MissileReferenceTransform = mTf; - ml.deployableRail = this; + ml.MissileReferenceTransform = mTf; + ml.deployableRail = this; - ml.decoupleForward = false; - ml.dropTime = Mathf.Max(ml.dropTime, 0.2f); + ml.decoupleForward = false; + ml.dropTime = Mathf.Max(ml.dropTime, 0.2f); - if (!comOffsets.ContainsKey(ml.part)) - { - comOffsets.Add(ml.part, ml.part.CoMOffset); + if (!comOffsets.ContainsKey(ml.part)) + { + comOffsets.Add(ml.part, ml.part.CoMOffset); + } + missileCount++; } - missileCount++; - } missileChildren = msl.ToArray(); missileCount = missileChildren.Length; @@ -380,6 +386,7 @@ public void FireMissile(int missileIndex) //this is causing it not to fire, dete if (weaponManager) { wm.SendTargetDataToMissile(missileChildren[missileIndex]); + wm.PreviousMissile = missileChildren[missileIndex]; } missileChildren[missileIndex].FireMissile(); diff --git a/BDArmory/Modules/BDRotaryRail.cs b/BDArmory/WeaponMounts/BDRotaryRail.cs similarity index 94% rename from BDArmory/Modules/BDRotaryRail.cs rename to BDArmory/WeaponMounts/BDRotaryRail.cs index 56cd46a79..4ff98daba 100644 --- a/BDArmory/Modules/BDRotaryRail.cs +++ b/BDArmory/WeaponMounts/BDRotaryRail.cs @@ -3,7 +3,12 @@ using UniLinq; using UnityEngine; -namespace BDArmory.Modules +using BDArmory.Control; +using BDArmory.Extensions; +using BDArmory.Utils; +using BDArmory.Weapons.Missiles; + +namespace BDArmory.WeaponMounts { public class BDRotaryRail : PartModule { @@ -257,7 +262,7 @@ public void MoveEndStackNode(float offset) IEnumerator DelayedMoveStackNode(float offset) { - yield return null; + yield return new WaitForFixedUpdate(); MoveEndStackNode(offset); } @@ -378,7 +383,7 @@ void UpdateChildrenHeight(float offset) { if (p.Current == null) continue; Vector3 direction = p.Current.transform.position - part.transform.position; - direction = Vector3.ProjectOnPlane(direction, part.transform.up).normalized; + direction = direction.ProjectOnPlanePreNormalized(part.transform.up).normalized; p.Current.transform.position += direction * offset; } @@ -458,6 +463,7 @@ public void RotateToMissile(MissileLauncher ml) for (int i = 0; i < missileChildren.Length; i++) { if (missileChildren[i].GetShortName() != ml.GetShortName()) continue; + if (missileChildren[i].HasFired) continue; RotateToIndex(missileToRailIndex[i], false); nextMissile = missileChildren[i]; return; @@ -487,13 +493,13 @@ void UpdateIndexDictionary() } missileToRailIndex.Add(i, rIndex); railToMissileIndex.Add(rIndex, i); - //Debug.Log("Adding to index dictionary: " + i + " : " + rIndex); + //Debug.Log("[BDArmory.BDRotaryRail]: Adding to index dictionary: " + i + " : " + rIndex); } } void RotateToIndex(int index, bool instant) { - //Debug.Log("Rotary rail is rotating to index: " + index); + //Debug.Log("[BDArmory.BDRotaryRail]: Rotary rail is rotating to index: " + index); if (rotationRoutine != null) { @@ -524,7 +530,7 @@ IEnumerator RotateToIndexRoutine(int index, bool instant) rdyMissile = null; railIndex = index; - yield return new WaitForSeconds(rotationDelay); + yield return new WaitForSecondsFixed(rotationDelay); Quaternion targetRot = Quaternion.Euler(0, 0, (float)index * -railAngle); @@ -574,7 +580,7 @@ public void PrepMissileForFire(MissileLauncher ml) { if (ml != readyMissile) { - //Debug.Log("Rotary rail tried prepping a missile for fire, but it is not in firing position"); + //Debug.Log("[BDArmory.BDRotaryRail]: Rotary rail tried prepping a missile for fire, but it is not in firing position"); return; } @@ -586,13 +592,13 @@ public void PrepMissileForFire(MissileLauncher ml) } else { - //Debug.Log("Tried to prep a missile for firing that doesn't exist or is not attached to the turret."); + //Debug.Log("[BDArmory.BDRotaryRail]: Tried to prep a missile for firing that doesn't exist or is not attached to the turret."); } } void PrepMissileForFire(int index) { - //Debug.Log("Prepping missile for rotary rail fire."); + //Debug.Log("[BDArmory.BDRotaryRail]: Prepping missile for rotary rail fire."); missileChildren[index].part.CoMOffset = comOffsets[missileChildren[index].part]; missileTransforms[index].localPosition = Vector3.zero; @@ -631,6 +637,7 @@ public void FireMissile(int missileIndex) if (weaponManager) { wm.SendTargetDataToMissile(missileChildren[missileIndex]); + wm.PreviousMissile = missileChildren[missileIndex]; } string firedMissileName = missileChildren[missileIndex].part.name; @@ -643,7 +650,7 @@ public void FireMissile(int missileIndex) nextRailIndex = Mathf.RoundToInt(Mathf.Repeat(missileToRailIndex[missileIndex] + 1, numberOfRails)); - UpdateMissileChildren(); + if (!missileChildren[missileIndex].reloadableRail) UpdateMissileChildren(); if (wm) { @@ -676,12 +683,12 @@ public void FireMissile(MissileLauncher ml) int index = IndexOfMissile(ml); if (index >= 0) { - //Debug.Log("Firing missile index: " + index); + //Debug.Log("[BDArmory.BDRotaryRail]: Firing missile index: " + index); FireMissile(index); } else { - //Debug.Log("Tried to fire a missile that doesn't exist or is not attached to the rail."); + //Debug.Log("[BDArmory.BDRotaryRail]: Tried to fire a missile that doesn't exist or is not attached to the rail."); } } @@ -755,7 +762,7 @@ public void UpdateMissileChildren() while (t.MoveNext()) { if (t.Current == null) continue; - //Debug.Log("MissileTurret moving transform: " + tfchildren[i].gameObject.name); + //Debug.Log("[BDArmory.BDRotaryRail]: MissileTurret moving transform: " + tfchildren[i].gameObject.name); t.Current.parent = mTf; } t.Dispose(); @@ -792,7 +799,7 @@ public void UpdateMissileChildren() UpdateIndexDictionary(); } - void UpdateMissilePositions() + public void UpdateMissilePositions() { if (missileCount == 0) { @@ -801,7 +808,7 @@ void UpdateMissilePositions() for (int i = 0; i < missileChildren.Length; i++) { - if (!missileTransforms[i] || !missileChildren[i] || missileChildren[i].HasFired) continue; + if (!missileTransforms[i] || !missileChildren[i]) continue; missileTransforms[i].position = missileReferenceTransforms[i].position; missileTransforms[i].rotation = missileReferenceTransforms[i].rotation; diff --git a/BDArmory/Modules/MissileTurret.cs b/BDArmory/WeaponMounts/MissileTurret.cs similarity index 92% rename from BDArmory/Modules/MissileTurret.cs rename to BDArmory/WeaponMounts/MissileTurret.cs index 679e707a0..d81eab2f3 100644 --- a/BDArmory/Modules/MissileTurret.cs +++ b/BDArmory/WeaponMounts/MissileTurret.cs @@ -1,13 +1,16 @@ using System.Collections; using System.Collections.Generic; -using BDArmory.Core; -using BDArmory.Core.Utils; -using BDArmory.Guidances; -using BDArmory.Misc; using UniLinq; using UnityEngine; -namespace BDArmory.Modules +using BDArmory.Control; +using BDArmory.Guidances; +using BDArmory.Radar; +using BDArmory.Settings; +using BDArmory.Utils; +using BDArmory.Weapons.Missiles; + +namespace BDArmory.WeaponMounts { public class MissileTurret : PartModule { @@ -81,14 +84,15 @@ public MissileFire weaponManager IEnumerator DeployAnimation(bool forward) { - yield return null; + var wait = new WaitForFixedUpdate(); + yield return wait; if (forward) { while (deployAnimState.normalizedTime < 1) { deployAnimState.speed = deployAnimationSpeed; - yield return null; + yield return wait; } deployAnimState.normalizedTime = 1; @@ -97,15 +101,12 @@ IEnumerator DeployAnimation(bool forward) { deployAnimState.speed = 0; - while (pausingAfterShot) - { - yield return new WaitForFixedUpdate(); - } + yield return new WaitWhileFixed(() => pausingAfterShot); while (deployAnimState.normalizedTime > 0) { deployAnimState.speed = -deployAnimationSpeed; - yield return null; + yield return wait; } deployAnimState.normalizedTime = 0; @@ -219,7 +220,7 @@ IEnumerator ReturnRoutine() yield break; } - yield return new WaitForSeconds(0.25f); + yield return new WaitForSecondsFixed(0.25f); while (pausingAfterShot) { @@ -243,7 +244,7 @@ public override void OnStart(StartState state) if (!string.IsNullOrEmpty(deployAnimationName)) { hasDeployAnimation = true; - deployAnimState = Utils.SetUpSingleAnimation(deployAnimationName, part); + deployAnimState = GUIUtils.SetUpSingleAnimation(deployAnimationName, part); if (state == StartState.Editor) { Events["EditorToggleAnimation"].guiActiveEditor = true; @@ -356,7 +357,7 @@ public void SlavedAim() turret.AimToTarget(slavedTargetPosition); } - int mouseAimLayerMask = (int)(LayerMasks.Parts | LayerMasks.Scenery | LayerMasks.EVA | LayerMasks.Unknown19 | LayerMasks.Unknown23); + const int mouseAimLayerMask = (int)(LayerMasks.Parts | LayerMasks.Scenery | LayerMasks.EVA | LayerMasks.Unknown19 | LayerMasks.Unknown23 | LayerMasks.Wheels); void MouseAim() { if (pausingAfterShot) return; @@ -457,7 +458,7 @@ public void UpdateMissileChildren() while (t.MoveNext()) { if (t.Current == null) continue; - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_OTHER) Debug.Log("[BDArmory.MissileTurret] : MissileTurret moving transform: " + t.Current.gameObject.name); t.Current.parent = mTf; } @@ -499,7 +500,7 @@ void UpdateMissilePositions() for (int i = 0; i < missileChildren.Length; i++) { - if (missileTransforms[i] && missileChildren[i] && !missileChildren[i].HasFired) + if (missileTransforms[i] && missileChildren[i])// && !missileChildren[i].HasFired) { missileTransforms[i].position = missileReferenceTransforms[i].position; missileTransforms[i].rotation = missileReferenceTransforms[i].rotation; @@ -525,15 +526,16 @@ public void FireMissile(int index) if (weaponManager) { wm.SendTargetDataToMissile(missileChildren[index]); + wm.PreviousMissile = missileChildren[index]; } missileChildren[index].FireMissile(); - StartCoroutine(MissileRailRoutine(missileChildren[index])); + StartCoroutine(MissileRailRoutine(missileChildren[index])); //turret is stil getting thrusted away despite this being behind a !relaodableRail conditional. investigate if (wm) { wm.UpdateList(); } - UpdateMissileChildren(); + if (!missileChildren[index].reloadableRail) UpdateMissileChildren(); timeFired = Time.time; } @@ -555,7 +557,8 @@ public void FireMissile(MissileLauncher ml) IEnumerator MissileRailRoutine(MissileLauncher ml) { - yield return null; + var wait = new WaitForFixedUpdate(); + yield return wait; Ray ray = new Ray(ml.transform.position, ml.MissileReferenceTransform.forward); Vector3 localOrigin = turret.pitchTransform.InverseTransformPoint(ray.origin); Vector3 localDirection = turret.pitchTransform.InverseTransformDirection(ray.direction); @@ -575,9 +578,9 @@ IEnumerator MissileRailRoutine(MissileLauncher ml) //Vector3 projVel = Vector3.Project(ml.vessel.Velocity-railVel, ray.direction); ml.vessel.SetPosition(projPos); - ml.vessel.SetWorldVelocity(railVel + (forwardSpeed * ray.direction)); - - yield return new WaitForFixedUpdate(); + if (!ml.reloadableRail) ml.vessel.SetWorldVelocity(railVel + (forwardSpeed * ray.direction)); //this is still imparting veloctity on spawned missiles? Can function without, as long as missile turret is a static SAM site or similar + //else ml.reloadableRail.SpawnedMissile.vessel.SetWorldVelocity(railVel + (forwardSpeed * ray.direction)); + yield return wait; ray.origin = turret.pitchTransform.TransformPoint(localOrigin); ray.direction = turret.pitchTransform.TransformDirection(localDirection); @@ -631,7 +634,7 @@ public bool ContainsMissileOfType(MissileLauncher ml) for (int i = 0; i < missileCount; i++) { - if ((missileChildren[i]) && missileChildren[i].part.name == ml.part.name) + if ((missileChildren[i]) && missileChildren[i].part.name == ml.part.name && !missileChildren[i].HasFired) { return true; } diff --git a/BDArmory/Modules/ModuleTurret.cs b/BDArmory/WeaponMounts/ModuleTurret.cs similarity index 79% rename from BDArmory/Modules/ModuleTurret.cs rename to BDArmory/WeaponMounts/ModuleTurret.cs index 568dda93d..649d070f3 100644 --- a/BDArmory/Modules/ModuleTurret.cs +++ b/BDArmory/WeaponMounts/ModuleTurret.cs @@ -1,10 +1,12 @@ using System; -using BDArmory.Core; -using BDArmory.Misc; -using BDArmory.UI; using UnityEngine; -namespace BDArmory.Modules +using BDArmory.Extensions; +using BDArmory.Settings; +using BDArmory.UI; +using BDArmory.Utils; + +namespace BDArmory.WeaponMounts { public class ModuleTurret : PartModule { @@ -81,7 +83,7 @@ public override void OnStart(StartState state) if (!string.IsNullOrEmpty(audioPath) && (yawSpeedDPS != 0 || pitchSpeedDPS != 0)) { - soundClip = GameDatabase.Instance.GetAudioClip(audioPath); + soundClip = SoundUtils.GetAudioClip(audioPath); audioSource = gameObject.AddComponent(); audioSource.clip = soundClip; @@ -170,13 +172,12 @@ public void AimInDirection(Vector3 targetDirection, bool pitch = true, bool yaw float deltaTime = Time.fixedDeltaTime; Vector3 yawNormal = yawTransform.up; - Vector3 yawComponent = Vector3.ProjectOnPlane(targetDirection, yawNormal); - Vector3 pitchNormal = Vector3.Cross(yawComponent, yawNormal); - Vector3 pitchComponent = Vector3.ProjectOnPlane(targetDirection, pitchNormal); + Vector3 yawComponent = targetDirection.ProjectOnPlanePreNormalized(yawNormal); + Vector3 pitchComponent = targetDirection.ProjectOnPlane(Vector3.Cross(yawComponent, yawNormal)); float currentYaw = yawTransform.localEulerAngles.y.ToAngle(); float yawError = VectorUtils.SignedAngleDP( - Vector3.ProjectOnPlane(referenceTransform.forward, yawNormal), + referenceTransform.forward.ProjectOnPlanePreNormalized(yawNormal), yawComponent, Vector3.Cross(yawNormal, referenceTransform.forward)); float yawOffset = Mathf.Abs(yawError); @@ -234,6 +235,9 @@ public void AimInDirection(Vector3 targetDirection, bool pitch = true, bool yaw Quaternion.Euler(-targetPitchAngle, 0, 0), pitchSpeed); } + public float Pitch => -pitchTransform.localEulerAngles.x.ToAngle(); + public float Yaw => yawTransform.localEulerAngles.y.ToAngle(); + public bool ReturnTurret() { if (!yawTransform) @@ -307,6 +311,8 @@ void SetupTweakables() } minPitchRange.minValue = minPitchLimit; minPitchRange.maxValue = 0; + if (minPitchLimit != 0) + minPitchRange.stepIncrement = Mathf.Pow(10, Mathf.Min(1f, Mathf.Floor(Mathf.Log10(Mathf.Abs(minPitchLimit)) + (1 - Mathf.Log10(20f) - 1e-4f)))) / 10f; // Use between 20 and 200 divisions UI_FloatRange maxPitchRange = (UI_FloatRange)Fields["maxPitch"].uiControlEditor; if (maxPitchLimit > 90) @@ -319,6 +325,8 @@ void SetupTweakables() } maxPitchRange.maxValue = maxPitchLimit; maxPitchRange.minValue = 0; + if (maxPitchLimit != 0) + maxPitchRange.stepIncrement = Mathf.Pow(10, Mathf.Min(1f, Mathf.Floor(Mathf.Log10(Mathf.Abs(maxPitchLimit)) + (1 - Mathf.Log10(20f) - 1e-4f)))) / 10f; // Use between 20 and 200 divisions UI_FloatRange yawRangeEd = (UI_FloatRange)Fields["yawRange"].uiControlEditor; if (yawRangeLimit > 360) @@ -342,6 +350,62 @@ void SetupTweakables() yawRangeEd.minValue = 0; yawRangeEd.maxValue = yawRangeLimit; } + if (yawRange != 0) + yawRangeEd.stepIncrement = Mathf.Pow(10, Math.Min(1f, Mathf.Floor(Mathf.Log10(Mathf.Abs(yawRange)) + (1 - Mathf.Log10(20f) - 1e-4f)))) / 10f; // Use between 20 and 200 divisions } } + public class BDAScaleByDistance : PartModule + { + /// + /// Sibling Module to FXModuleLookAtConstraint, causes indicated mesh object to scale based on distance to target transform + /// Module ported over to fix the spring on the M230Chaingun (no Stock equivalent), though I guess it could be used for other things as well + /// + [KSPField(isPersistant = false)] + public string transformToScaleName; + + public Transform transformToScale; + + [KSPField(isPersistant = false)] + public string scaleFactor = "0,0,1"; + Vector3 scaleFactorV; + + [KSPField(isPersistant = false)] + public string distanceTransformName; + + public Transform distanceTransform; + + + public override void OnStart(PartModule.StartState state) + { + ParseScale(); + transformToScale = part.FindModelTransform(transformToScaleName); + distanceTransform = part.FindModelTransform(distanceTransformName); + } + + public void Update() + { + Vector3 finalScaleFactor; + float distance = Vector3.Distance(transformToScale.position, distanceTransform.position); + float sfX = (scaleFactorV.x != 0) ? scaleFactorV.x * distance : 1; + float sfY = (scaleFactorV.y != 0) ? scaleFactorV.y * distance : 1; + float sfZ = (scaleFactorV.z != 0) ? scaleFactorV.z * distance : 1; + finalScaleFactor = new Vector3(sfX, sfY, sfZ); + + transformToScale.localScale = finalScaleFactor; + } + + + + void ParseScale() + { + string[] split = scaleFactor.Split(','); + float[] splitF = new float[split.Length]; + for (int i = 0; i < split.Length; i++) + { + splitF[i] = float.Parse(split[i]); + } + scaleFactorV = new Vector3(splitF[0], splitF[1], splitF[2]); + } + + } } diff --git a/BDArmory/Misc/WMTurretGroup.cs b/BDArmory/WeaponMounts/WMTurretGroup.cs similarity index 95% rename from BDArmory/Misc/WMTurretGroup.cs rename to BDArmory/WeaponMounts/WMTurretGroup.cs index 27201d7c7..df115d296 100644 --- a/BDArmory/Misc/WMTurretGroup.cs +++ b/BDArmory/WeaponMounts/WMTurretGroup.cs @@ -1,8 +1,9 @@ using System.Collections.Generic; -using BDArmory.Modules; using UnityEngine; -namespace BDArmory.Misc +using BDArmory.Weapons; + +namespace BDArmory.WeaponMounts { public class WMTurretGroup : MonoBehaviour { diff --git a/BDArmory/Modules/BDExplosivePart.cs b/BDArmory/Weapons/BDExplosivePart.cs similarity index 65% rename from BDArmory/Modules/BDExplosivePart.cs rename to BDArmory/Weapons/BDExplosivePart.cs index 7717ee61e..e62e2c64f 100644 --- a/BDArmory/Modules/BDExplosivePart.cs +++ b/BDArmory/Weapons/BDExplosivePart.cs @@ -1,15 +1,15 @@ -using BDArmory.Core; -using BDArmory.Core.Extension; -using BDArmory.Core.Utils; -using BDArmory.Control; -using BDArmory.FX; -using BDArmory.Misc; using KSP.Localization; -using System.Collections.Generic; using System.Linq; using UnityEngine; -namespace BDArmory.Modules +using BDArmory.Control; +using BDArmory.Extensions; +using BDArmory.FX; +using BDArmory.Settings; +using BDArmory.Utils; +using BDArmory.Weapons.Missiles; + +namespace BDArmory.Weapons { public class BDExplosivePart : PartModule { @@ -32,6 +32,9 @@ public class BDExplosivePart : PartModule [KSPField(guiActive = false, guiActiveEditor = false, guiName = "#LOC_BDArmory_Status")]//Status public string guiStatusString = "ARMED"; + [KSPField] + public float fuseFailureRate = 0f; // How often the explosive fuse will fail to detonate (0-1), evaluated once on detonation trigger + //PartWindow buttons [KSPEvent(guiActive = false, guiActiveEditor = false, guiName = "Disarm Warhead")]//Toggle public void Toggle() @@ -40,12 +43,12 @@ public void Toggle() if (Armed) { guiStatusString = "ARMED"; - Events["Toggle"].guiName = Localizer.Format("Disarm Warhead");//"Enable Engage Options" + Events["Toggle"].guiName = StringUtils.Localize("Disarm Warhead");//"Enable Engage Options" } else { guiStatusString = "Safe"; - Events["Toggle"].guiName = Localizer.Format("Arm Warhead");//"Disable Engage Options" + Events["Toggle"].guiName = StringUtils.Localize("Arm Warhead");//"Disable Engage Options" } } @@ -60,12 +63,12 @@ public void ToggleIFF() if (IFF_On) { guiIFFString = "Ignore Allies"; - Events["ToggleIFF"].guiName = Localizer.Format("Disable IFF");//"Enable Engage Options" + Events["ToggleIFF"].guiName = StringUtils.Localize("Disable IFF");//"Enable Engage Options" } else { guiIFFString = "Indescriminate"; - Events["ToggleIFF"].guiName = Localizer.Format("Enable IFF");//"Disable Engage Options" + Events["ToggleIFF"].guiName = StringUtils.Localize("Enable IFF");//"Disable Engage Options" } } @@ -85,13 +88,19 @@ public void ToggleProx() Fields["detonationRange"].guiActiveEditor = false; Fields["detonationRange"].guiActive = false; } - Utils.RefreshAssociatedWindows(part); + GUIUtils.RefreshAssociatedWindows(part); } [KSPField] public string warheadType = "standard"; public string warheadReportingName; + [KSPField] + public float caliber = 120; + + [KSPField] + public float apMod = 1; + [KSPField] public string explModelPath = "BDArmory/Models/explosion/explosion"; @@ -103,7 +112,7 @@ public void ArmAG(KSPActionParam param) { Armed = true; guiStatusString = "ARMED"; // Future me, this needs localization at some point - Events["Toggle"].guiName = Localizer.Format("Disarm Warhead");//"Enable Engage Options" + Events["Toggle"].guiName = StringUtils.Localize("Disarm Warhead");//"Enable Engage Options" } [KSPAction("Detonate")] @@ -134,6 +143,10 @@ public void DetonateEvent() private double previousMass = -1; public bool hasDetonated; + public bool fuseFailed = false; + Collider[] proximityHitColliders = new Collider[100]; + + public Vector3 direction; public override void OnStart(StartState state) { @@ -163,6 +176,13 @@ public override void OnStart(StartState state) */ CalculateBlast(); ParseWarheadType(); + if (HighLogic.LoadedSceneIsFlight) + GameEvents.onGameSceneSwitchRequested.Add(HandleSceneChange); + } + + void OnDestroy() + { + GameEvents.onGameSceneSwitchRequested.Remove(HandleSceneChange); } public void GuiSetup() @@ -182,22 +202,22 @@ public void GuiSetup() if (Armed) { guiStatusString = "ARMED"; - Events["Toggle"].guiName = Localizer.Format("Disarm Warhead"); + Events["Toggle"].guiName = StringUtils.Localize("Disarm Warhead"); } else { guiStatusString = "Safe"; - Events["Toggle"].guiName = Localizer.Format("Arm Warhead"); + Events["Toggle"].guiName = StringUtils.Localize("Arm Warhead"); } if (IFF_On) { guiIFFString = "Ignore Allies"; - Events["ToggleIFF"].guiName = Localizer.Format("Disable IFF"); + Events["ToggleIFF"].guiName = StringUtils.Localize("Disable IFF"); } else { guiIFFString = "Indescriminate"; - Events["ToggleIFF"].guiName = Localizer.Format("Enable IFF"); + Events["ToggleIFF"].guiName = StringUtils.Localize("Enable IFF"); } if (manualOverride) { @@ -228,7 +248,7 @@ public void GuiSetup() Fields["detonateAtMinimumDistance"].guiActiveEditor = false; Fields["detonateAtMinimumDistance"].guiActive = false; } - Utils.RefreshAssociatedWindows(part); + GUIUtils.RefreshAssociatedWindows(part); } public void Update() @@ -237,44 +257,38 @@ public void Update() { OnUpdateEditor(); } - if (hasDetonated) - { - this.part.explode(); - } } public override void OnFixedUpdate() { base.OnFixedUpdate(); - if (HighLogic.LoadedSceneIsFlight) + if (!HighLogic.LoadedSceneIsFlight) return; + if (!isMissile) { - if (!isMissile) + if (IFF_On) { - if (IFF_On) + updateTimer -= Time.fixedDeltaTime; + if (updateTimer < 0) { - updateTimer -= Time.fixedDeltaTime; - if (updateTimer < 0) - { - GetTeamID(); //have this only called once a sec - updateTimer = 1.0f; //next update in half a sec only - } + GetTeamID(); //have this only called once a sec + updateTimer = 1.0f; //next update in half a sec only } - if (manualOverride) // don't call proximity code if a missile/MMG, use theirs + } + if (manualOverride) // don't call proximity code if a missile/MMG, use theirs + { + if (Armed) { - if (Armed) + if (VesselModuleRegistry.GetModule(vessel) == null) { - if (VesselModuleRegistry.GetModule(vessel) == null) + if (sourcevessel != null && sourcevessel != part.vessel) { - if (sourcevessel != null && sourcevessel != part.vessel) - { - distanceFromStart = Vector3.Distance(part.vessel.transform.position, sourcevessel.transform.position); - } - } - if (Checkproximity(distanceFromStart)) - { - Detonate(); + distanceFromStart = Vector3.Distance(part.vessel.transform.position, sourcevessel.transform.position); } } + if (Checkproximity(distanceFromStart)) + { + Detonate(); + } } } } @@ -321,22 +335,32 @@ public void ParseWarheadType() break; } } + public void DetonateIfPossible() { - if (part == null) return; + if (!HighLogic.LoadedSceneIsFlight || part == null) return; if (!hasDetonated && Armed) { - Vector3 direction = default(Vector3); + hasDetonated = true; - if (warheadType != "standard") + if (fuseFailureRate > 0f) + if (Random.Range(0f, 1f) < fuseFailureRate) + fuseFailed = true; + + if (!fuseFailed) { - direction = (part.transform.position + part.rb.velocity * Time.deltaTime).normalized; + direction = warheadType == "standard" ? default : part.partTransform.forward; //both the missileReferenceTransform and smallWarhead part's forward direction is Z+, or transform.forward. + // could also do warheadType == "standard" ? default: part.partTransform.forward, as this simplifies the isAngleAllowed check in ExplosionFX, but at the cost of standard heads always being 360deg blasts (but we don't have limited angle balsts for missiels at present anyway, so not a bit deal RN) + var sourceWeapon = part.FindModuleImplementing(); + + ExplosionFx.CreateExplosion(part.transform.position, tntMass, explModelPath, explSoundPath, ExplosionSourceType.Missile, caliber, part, sourcevessel != null ? sourcevessel.vesselName : null, sourceWeapon != null ? sourceWeapon.GetShortName() : null, direction, -1, false, warheadType == "standard" ? part.mass : 0, -1, 1, warheadType, null, apMod); + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log("[BDArmory.BDExplosivePart]: " + part + " (" + (uint)(part.GetInstanceID()) + ") from " + (sourcevessel != null ? sourcevessel.vesselName : null) + " detonating with a " + warheadType + " warhead"); + part.explode(); } - var sourceWeapon = part.FindModuleImplementing(); - ExplosionFx.CreateExplosion(part.transform.position, tntMass, explModelPath, explSoundPath, ExplosionSourceType.Missile, 120, part, sourcevessel != null ? sourcevessel.vesselName : null, sourceWeapon != null ? sourceWeapon.GetShortName() : null, direction, -1, false, warheadType == "standard" ? part.mass : 0, -1, 1, warheadType); - hasDetonated = true; - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log("[BDArmory.BDExplosivePart]: " + part + " (" + (uint)(part.GetInstanceID()) + ") from " + (sourcevessel != null ? sourcevessel.vesselName : null) + " detonating with a " + warheadType + " warhead"); + else + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log("[BDArmory.BDExplosivePart]: " + part + " (" + (uint)(part.GetInstanceID()) + ") from " + (sourcevessel != null ? sourcevessel.vesselName : null) + " explosive fuse failed!"); } } @@ -344,21 +368,35 @@ private void Detonate() { if (!hasDetonated && Armed) { - Vector3 direction = default(Vector3); + hasDetonated = true; + + if (fuseFailureRate > 0f) + if (Random.Range(0f, 1f) < fuseFailureRate) + fuseFailed = true; - if (warheadType != "standard") + if (!fuseFailed) { - direction = (part.transform.position + part.rb.velocity * Time.deltaTime).normalized; + direction = warheadType == "standard" ? default : part.partTransform.forward; + var sourceWeapon = part.FindModuleImplementing(); + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log("[BDArmory.BDExplosivePart]: " + part + " (" + (uint)(part.GetInstanceID()) + ") from " + (sourcevessel != null ? sourcevessel.vesselName : null) + " detonating with a " + warheadType + " warhead"); + ExplosionFx.CreateExplosion(part.transform.position, tntMass, explModelPath, explSoundPath, ExplosionSourceType.Missile, caliber, part, sourcevessel != null ? sourcevessel.vesselName : null, sourceWeapon != null ? sourceWeapon.GetShortName() : null, direction, -1, false, warheadType == "standard" ? part.mass : 0, -1, 1, warheadType, null, apMod); + + part.Destroy(); + part.explode(); } - var sourceWeapon = part.FindModuleImplementing(); - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log("[BDArmory.BDExplosivePart]: " + part + " (" + (uint)(part.GetInstanceID()) + ") from " + (sourcevessel != null ? sourcevessel.vesselName : null) + " detonating with a " + warheadType + " warhead"); - ExplosionFx.CreateExplosion(part.transform.position, tntMass, explModelPath, explSoundPath, ExplosionSourceType.Missile, 120, part, sourcevessel != null ? sourcevessel.vesselName : null, sourceWeapon != null ? sourceWeapon.GetShortName() : null, direction, -1, false, warheadType == "standard" ? part.mass : 0, -1, 1, warheadType); - hasDetonated = true; - part.Destroy(); + else + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log("[BDArmory.BDExplosivePart]: " + part + " (" + (uint)(part.GetInstanceID()) + ") from " + (sourcevessel != null ? sourcevessel.vesselName : null) + " explosive fuse failed!"); } } + public void HandleSceneChange(GameEvents.FromToAction fromTo) + { + if (fromTo.from == GameScenes.FLIGHT) + { hasDetonated = true; } // Don't trigger explosions on scene changes. + } + public float GetBlastRadius() { CalculateBlast(); @@ -383,7 +421,14 @@ private bool Checkproximity(float distanceFromStart) return detonate = false; } - using (var hitsEnu = Physics.OverlapSphere(transform.position, detonationRange, (int)(LayerMasks.Parts | LayerMasks.Scenery | LayerMasks.Unknown19)).AsEnumerable().GetEnumerator()) + var layerMask = (int)(LayerMasks.Parts | LayerMasks.Scenery | LayerMasks.Unknown19 | LayerMasks.Wheels); + var hitCount = Physics.OverlapSphereNonAlloc(transform.position, detonationRange, proximityHitColliders, layerMask); + if (hitCount == proximityHitColliders.Length) + { + proximityHitColliders = Physics.OverlapSphere(transform.position, detonationRange, layerMask); + hitCount = proximityHitColliders.Length; + } + using (var hitsEnu = proximityHitColliders.Take(hitCount).GetEnumerator()) { while (hitsEnu.MoveNext()) { @@ -406,7 +451,7 @@ private bool Checkproximity(float distanceFromStart) return detonate = false; } } - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BDExplosivePart]: Proxifuze triggered by " + partHit.partName + " from " + partHit.vessel.vesselName); + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.BDExplosivePart]: Proxifuze triggered by " + partHit.partName + " from " + partHit.vessel.vesselName); return detonate = true; } } diff --git a/BDArmory/Modules/BDModuleNuke.cs b/BDArmory/Weapons/BDModuleNuke.cs similarity index 73% rename from BDArmory/Modules/BDModuleNuke.cs rename to BDArmory/Weapons/BDModuleNuke.cs index f955f1e91..914e9361d 100644 --- a/BDArmory/Modules/BDModuleNuke.cs +++ b/BDArmory/Weapons/BDModuleNuke.cs @@ -1,15 +1,19 @@ -using BDArmory.Competition; -using BDArmory.Competition.VesselSpawning; -using BDArmory.Core; -using BDArmory.Core.Extension; -using BDArmory.Core.Module; -using BDArmory.FX; using System; using System.Collections; using System.Text; using UnityEngine; -namespace BDArmory.Modules +using BDArmory.Competition; +using BDArmory.Control; +using BDArmory.Damage; +using BDArmory.Extensions; +using BDArmory.FX; +using BDArmory.Settings; +using BDArmory.Utils; +using BDArmory.VesselSpawning; +using BDArmory.Weapons.Missiles; + +namespace BDArmory.Weapons { class BDModuleNuke : PartModule { @@ -17,7 +21,7 @@ class BDModuleNuke : PartModule public string status = "OFFLINE"; [KSPField(isPersistant = true, guiActive = true, guiName = "Coolant Remaining", guiActiveEditor = false), UI_Label(scene = UI_Scene.All)] - public double fuelleft; + public double fuelleft = 0; public static string defaultflashModelPath = "BDArmory/Models/explosion/nuke/nukeFlash"; [KSPField] @@ -62,6 +66,7 @@ class BDModuleNuke : PartModule public float meltDownDuration = 2.5f; private int FuelID; + private int MPID; private bool hasDetonated = false; private bool goingCritical = false; public string Sourcevessel; @@ -80,15 +85,21 @@ public MissileLauncher Launcher } } - public override void OnStart(StartState state) + public void Start() { if (HighLogic.LoadedSceneIsFlight) { if (engineCore) { FuelID = PartResourceLibrary.Instance.GetDefinition("LiquidFuel").id; + Debug.Log($"[BDArmory.BDModuleNuke]: Resource definition for LiquidFuel is" + FuelID); vessel.GetConnectedResourceTotals(FuelID, out double fuelCurrent, out double fuelMax); fuelleft = fuelCurrent; + Debug.Log($"[BDArmory.BDModuleNuke]: Found {fuelMax} LF on {part.vessel.GetName()}"); + MPID = PartResourceLibrary.Instance.GetDefinition("MonoPropellant").id; + vessel.GetConnectedResourceTotals(MPID, out double mpCurrent, out double mpMax); + fuelleft += mpCurrent; + Debug.Log($"[BDArmory.BDModuleNuke]: Found {mpMax} MP on {part.vessel.GetName()}"); var engine = part.FindModuleImplementing(); if (engine != null) { @@ -100,7 +111,8 @@ public override void OnStart(StartState state) { Fields["status"].guiActive = false; Fields["fuelleft"].guiActive = false; - var missile = part.FindModuleImplementing(); + Fields["status"].guiActiveEditor = false; + Fields["fuelleft"].guiActiveEditor = false; } Sourcevessel = part.vessel.GetName(); @@ -108,10 +120,9 @@ public override void OnStart(StartState state) GameEvents.onVesselPartCountChanged.Add(CheckAttached); GameEvents.onVesselCreate.Add(CheckAttached); } - base.OnStart(state); } - public void Update() + public void FixedUpdate() { if (HighLogic.LoadedSceneIsFlight) { @@ -121,11 +132,13 @@ public void Update() { vessel.GetConnectedResourceTotals(FuelID, out double fuelCurrent, out double fuelMax); fuelleft = fuelCurrent; + vessel.GetConnectedResourceTotals(MPID, out double mpCurrent, out double mpMax); + fuelleft += mpCurrent; if (fuelleft <= 0) { if (!hasDetonated && !goingCritical) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.RWPS3R2NukeModule]: nerva on " + Sourcevessel + " is out of fuel."); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log("[BDArmory.RWPS3R2NukeModule]: nerva on " + (String.IsNullOrEmpty(Sourcevessel)? Sourcevessel : part.vessel.GetName()) + " is out of fuel."); StartCoroutine(DelayedDetonation(meltDownDuration)); //bingo fuel, detonate } } @@ -136,7 +149,7 @@ public void Update() { if (!hasDetonated) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.RWPS3R2NukeModule]: nerva on " + Sourcevessel + " is Off, detonating"); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log("[BDArmory.RWPS3R2NukeModule]: nerva on " + Sourcevessel + " is Off, detonating"); Detonate(); //nuke engine off after comp start, detonate. } } @@ -146,7 +159,7 @@ public void Update() { if (!hasDetonated) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.RWPS3R2NukeModule]: nerva on " + Sourcevessel + " is manually thrust limited, detonating"); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log("[BDArmory.RWPS3R2NukeModule]: nerva on " + Sourcevessel + " is manually thrust limited, detonating"); Detonate(); //nuke engine off after comp start, detonate. } } @@ -163,7 +176,7 @@ void CheckAttached(Vessel v) VesselModuleRegistry.OnVesselModified(v); if (VesselModuleRegistry.GetModuleCount(v) == 0) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.RWPS3R2NukeModule]: Nuclear engine on " + Sourcevessel + " has become detached."); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log("[BDArmory.RWPS3R2NukeModule]: Nuclear engine on " + Sourcevessel + " has become detached."); goingCritical = true; StartCoroutine(DelayedDetonation(0.5f)); } @@ -171,9 +184,9 @@ void CheckAttached(Vessel v) IEnumerator DelayedDetonation(float delay) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.RWPS3R2NukeModule]: Nuclear engine on " + Sourcevessel + " going critical in " + delay.ToString("0.0") + "s."); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log("[BDArmory.RWPS3R2NukeModule]: Nuclear engine on " + Sourcevessel + " going critical in " + delay.ToString("0.0") + "s."); goingCritical = true; - yield return new WaitForSeconds(delay); + yield return new WaitForSecondsFixed(delay); if (!hasDetonated && part != null) Detonate(); } @@ -189,14 +202,14 @@ public void Detonate() { return; } - if (missile != null && - (missile.MissileState == MissileBase.MissileStates.Idle || missile.MissileState == MissileBase.MissileStates.Drop)) + if (Launcher != null && + (Launcher.MissileState == MissileBase.MissileStates.Idle || Launcher.MissileState == MissileBase.MissileStates.Drop)) { return; } - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.BDModuleNuke]: Running Detonate() on nukeModule in vessel " + Sourcevessel); + if (BDArmorySettings.DEBUG_OTHER) Debug.Log("[BDArmory.BDModuleNuke]: Running Detonate() on nukeModule in vessel " + Sourcevessel); //affect any nearby parts/vessels that aren't the source vessel - NukeFX.CreateExplosion(part.transform.position, ExplosionSourceType.BattleDamage, Sourcevessel, reportingName, 0, thermalRadius, yield, fluence, isEMP, blastSoundPath, flashModelPath, shockModelPath, blastModelPath, plumeModelPath, debrisModelPath, "", ""); + NukeFX.CreateExplosion(part.transform.position, Launcher != null ? ExplosionSourceType.Missile : ExplosionSourceType.BattleDamage, Sourcevessel, reportingName, 0, thermalRadius, yield, fluence, isEMP, blastSoundPath, flashModelPath, shockModelPath, blastModelPath, plumeModelPath, debrisModelPath, "", ""); hasDetonated = true; if (part.vessel != null) // Already in the process of being destroyed. part.Destroy(); @@ -212,7 +225,7 @@ public override string GetInfo() output.AppendLine($"Yield: {yield}"); output.AppendLine($"Generates EMP: {isEMP}"); } - if (missile != null) + if (Launcher != null) { output.AppendLine($"Nuclear Warhead"); output.AppendLine($"Yield: {yield}"); diff --git a/BDArmory/Modules/ClusterBomb.cs b/BDArmory/Weapons/ClusterBomb.cs similarity index 90% rename from BDArmory/Modules/ClusterBomb.cs rename to BDArmory/Weapons/ClusterBomb.cs index 96e1fee39..09e520b24 100644 --- a/BDArmory/Modules/ClusterBomb.cs +++ b/BDArmory/Weapons/ClusterBomb.cs @@ -1,13 +1,14 @@ using System; using System.Collections.Generic; -using BDArmory.Core.Extension; -using BDArmory.Core.Utils; -using BDArmory.FX; -using BDArmory.Misc; using UniLinq; using UnityEngine; -namespace BDArmory.Modules +using BDArmory.Extensions; +using BDArmory.Utils; +using BDArmory.FX; +using BDArmory.Weapons.Missiles; + +namespace BDArmory.Weapons { public class ClusterBomb : PartModule { @@ -93,7 +94,7 @@ public override void OnFixedUpdate() void DeploySubmunitions() { - missileLauncher.sfAudioSource.PlayOneShot(GameDatabase.Instance.GetAudioClip("BDArmory/Sounds/flare")); + missileLauncher.sfAudioSource.PlayOneShot(SoundUtils.GetAudioClip("BDArmory/Sounds/flareSound")); FXMonger.Explode(part, transform.position + part.rb.velocity * Time.fixedDeltaTime, 0.1f); deployed = true; @@ -119,7 +120,7 @@ void DeploySubmunitions() Vector3 direction = (sub.Current.transform.position - part.transform.position).normalized; Rigidbody subRB = sub.Current.GetComponent(); subRB.isKinematic = false; - subRB.velocity = part.rb.velocity + Krakensbane.GetFrameVelocityV3f() + + subRB.velocity = part.rb.velocity + BDKrakensbane.FrameVelocityV3f + (UnityEngine.Random.Range(submunitionMaxSpeed / 10, submunitionMaxSpeed) * direction); Submunition subScript = sub.Current.AddComponent(); @@ -141,7 +142,7 @@ void DeploySubmunitions() Vector3 direction = (fairing.Current.transform.position - part.transform.position).normalized; Rigidbody fRB = fairing.Current.GetComponent(); fRB.isKinematic = false; - fRB.velocity = part.rb.velocity + Krakensbane.GetFrameVelocityV3f() + ((submunitionMaxSpeed + 2) * direction); + fRB.velocity = part.rb.velocity + BDKrakensbane.FrameVelocityV3f + ((submunitionMaxSpeed + 2) * direction); fairing.Current.AddComponent(); fairing.Current.GetComponent().drag = 0.2f; ClusterBombFairing fairingScript = fairing.Current.AddComponent(); @@ -178,7 +179,7 @@ public class Submunition : MonoBehaviour float startTime; Rigidbody rb; - private int explosionLayerMask = (int)(LayerMasks.Parts | LayerMasks.Scenery | LayerMasks.EVA | LayerMasks.Unknown19 | LayerMasks.Unknown23); + const int explosionLayerMask = (int)(LayerMasks.Parts | LayerMasks.Scenery | LayerMasks.EVA | LayerMasks.Unknown19 | LayerMasks.Unknown23 | LayerMasks.Wheels); void Start() { @@ -206,10 +207,10 @@ void FixedUpdate() } //floating origin and velocity offloading corrections - if (!FloatingOrigin.Offset.IsZero() || !Krakensbane.GetFrameVelocity().IsZero()) + if (BDKrakensbane.IsActive) { - transform.position -= FloatingOrigin.OffsetNonKrakensbane; - prevPosition -= FloatingOrigin.OffsetNonKrakensbane; + transform.position -= BDKrakensbane.FloatingOriginOffsetNonKrakensbane; + prevPosition -= BDKrakensbane.FloatingOriginOffsetNonKrakensbane; } currPosition = transform.position; @@ -283,7 +284,7 @@ public class ClusterBombFairing : MonoBehaviour float startTime; Rigidbody rb; - private int explosionLayerMask = (int)(LayerMasks.Parts | LayerMasks.Scenery | LayerMasks.EVA | LayerMasks.Unknown19 | LayerMasks.Unknown23); + const int explosionLayerMask = (int)(LayerMasks.Parts | LayerMasks.Scenery | LayerMasks.EVA | LayerMasks.Unknown19 | LayerMasks.Unknown23 | LayerMasks.Wheels); void Start() { @@ -298,10 +299,10 @@ void FixedUpdate() if (deployed) { //floating origin and velocity offloading corrections - if (!FloatingOrigin.Offset.IsZero() || !Krakensbane.GetFrameVelocity().IsZero()) + if (BDKrakensbane.IsActive) { - transform.position -= FloatingOrigin.OffsetNonKrakensbane; - prevPosition -= FloatingOrigin.OffsetNonKrakensbane; + transform.position -= BDKrakensbane.FloatingOriginOffsetNonKrakensbane; + prevPosition -= BDKrakensbane.FloatingOriginOffsetNonKrakensbane; } currPosition = transform.position; diff --git a/BDArmory/Modules/EngageableWeapon.cs b/BDArmory/Weapons/EngageableWeapon.cs similarity index 76% rename from BDArmory/Modules/EngageableWeapon.cs rename to BDArmory/Weapons/EngageableWeapon.cs index 1017f1709..77abdba41 100644 --- a/BDArmory/Modules/EngageableWeapon.cs +++ b/BDArmory/Weapons/EngageableWeapon.cs @@ -1,7 +1,9 @@ using KSP.Localization; -using BDArmory.Misc; -namespace BDArmory.Modules +using BDArmory.Services; +using BDArmory.Utils; + +namespace BDArmory.Weapons { public abstract class EngageableWeapon : PartModule, IEngageService { @@ -40,11 +42,11 @@ public void ToggleEngageOptions() if (engageEnabled == false) { - Events["ToggleEngageOptions"].guiName = Localizer.Format("#LOC_BDArmory_EnableEngageOptions");//"Enable Engage Options" + Events["ToggleEngageOptions"].guiName = StringUtils.Localize("#LOC_BDArmory_EnableEngageOptions");//"Enable Engage Options" } else { - Events["ToggleEngageOptions"].guiName = Localizer.Format("#LOC_BDArmory_DisableEngageOptions");//"Disable Engage Options" + Events["ToggleEngageOptions"].guiName = StringUtils.Localize("#LOC_BDArmory_DisableEngageOptions");//"Disable Engage Options" } Fields["engageRangeMin"].guiActive = engageEnabled; @@ -60,7 +62,7 @@ public void ToggleEngageOptions() Fields["engageSLW"].guiActive = engageEnabled; Fields["engageSLW"].guiActiveEditor = engageEnabled; - Utils.RefreshAssociatedWindows(part); + GUIUtils.RefreshAssociatedWindows(part); } public void HideEngageOptions() { @@ -79,7 +81,7 @@ public void HideEngageOptions() Fields["engageSLW"].guiActive = false; Fields["engageSLW"].guiActiveEditor = false; - Utils.RefreshAssociatedWindows(part); + GUIUtils.RefreshAssociatedWindows(part); } public void OnRangeUpdated(BaseField field, object obj) { @@ -88,6 +90,35 @@ public void OnRangeUpdated(BaseField field, object obj) engageRangeMax = engageRangeMin; } + void OnEngageOptionsChanged(BaseField field, object obj) + { + var wm = VesselModuleRegistry.GetMissileFire(vessel, true); + var value = (bool)field.GetValue(this); + foreach (var part in part.symmetryCounterparts) + { + var engageableWeapon = part.GetComponent(); + if (engageableWeapon is not null) + { + field.SetValue(value, engageableWeapon); + } + } + + if (wm is not null) wm.weaponsListNeedsUpdating = true; + } + + public override void OnStart(StartState state) + { + base.OnStart(state); + var engageAirField = (UI_Toggle)Fields["engageAir"].uiControlFlight; + engageAirField.onFieldChanged = OnEngageOptionsChanged; + var engageMissileField = (UI_Toggle)Fields["engageMissile"].uiControlFlight; + engageMissileField.onFieldChanged = OnEngageOptionsChanged; + var engageGroundField = (UI_Toggle)Fields["engageGround"].uiControlFlight; + engageGroundField.onFieldChanged = OnEngageOptionsChanged; + var engageSLWField = (UI_Toggle)Fields["engageSLW"].uiControlFlight; + engageSLWField.onFieldChanged = OnEngageOptionsChanged; + } + protected void InitializeEngagementRange(float min, float max) { UI_FloatRange rangeMin = (UI_FloatRange)Fields["engageRangeMin"].uiControlEditor; diff --git a/BDArmory/Misc/IBDWeapon.cs b/BDArmory/Weapons/IBDWeapon.cs similarity index 83% rename from BDArmory/Misc/IBDWeapon.cs rename to BDArmory/Weapons/IBDWeapon.cs index 44ea295e0..057920983 100644 --- a/BDArmory/Misc/IBDWeapon.cs +++ b/BDArmory/Weapons/IBDWeapon.cs @@ -1,4 +1,4 @@ -namespace BDArmory.Misc +namespace BDArmory.Weapons { public interface IBDWeapon { @@ -8,8 +8,12 @@ public interface IBDWeapon string GetSubLabel(); + float GetEngageRange(); + string GetMissileType(); + string GetPartName(); + Part GetPart(); // extensions for feature_engagementenvelope diff --git a/BDArmory/Modules/BDMMLauncher.cs b/BDArmory/Weapons/Missiles/BDMMLauncher.cs similarity index 91% rename from BDArmory/Modules/BDMMLauncher.cs rename to BDArmory/Weapons/Missiles/BDMMLauncher.cs index 9ef36c068..c017892a0 100644 --- a/BDArmory/Modules/BDMMLauncher.cs +++ b/BDArmory/Weapons/Missiles/BDMMLauncher.cs @@ -1,6 +1,9 @@ using UnityEngine; -namespace BDArmory.Modules +using BDArmory.Control; +using BDArmory.Utils; + +namespace BDArmory.Weapons.Missiles { public class BDMMLauncher : PartModule { diff --git a/BDArmory/Modules/BDModularGuidance.cs b/BDArmory/Weapons/Missiles/BDModularGuidance.cs similarity index 82% rename from BDArmory/Modules/BDModularGuidance.cs rename to BDArmory/Weapons/Missiles/BDModularGuidance.cs index e1e470d99..b43ecd669 100644 --- a/BDArmory/Modules/BDModularGuidance.cs +++ b/BDArmory/Weapons/Missiles/BDModularGuidance.cs @@ -1,18 +1,20 @@ -using System; +using KSP.UI.Screens; using System.Collections.Generic; -using BDArmory.Core; -using BDArmory.Core.Extension; +using System; +using UniLinq; +using UnityEngine; + +using BDArmory.Control; +using BDArmory.Extensions; using BDArmory.Guidances; -using BDArmory.Misc; using BDArmory.Radar; +using BDArmory.Settings; using BDArmory.Targeting; using BDArmory.UI; -using KSP.UI.Screens; -using SaveUpgradePipeline; -using UniLinq; -using UnityEngine; +using BDArmory.Utils; +using BDArmory.VesselSpawning; -namespace BDArmory.Modules +namespace BDArmory.Weapons.Missiles { public class BDModularGuidance : MissileBase { @@ -56,9 +58,12 @@ public class BDModularGuidance : MissileBase [KSPField(isPersistant = true)] public int GuidanceIndex = 2; - [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_ActiveRadarRange"), UI_FloatRange(minValue = 6000f, maxValue = 50000f, stepIncrement = 1000f, scene = UI_Scene.Editor, affectSymCounterparts = UI_Scene.All)]//Active Radar Range + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_ActiveRadarRange"), UI_FloatRange(minValue = 0, maxValue = 50000f, stepIncrement = 1000f, scene = UI_Scene.Editor, affectSymCounterparts = UI_Scene.All)]//Active Radar Range public float ActiveRadarRange = 6000; + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_ChaffFactor"), UI_FloatRange(minValue = 0, maxValue = 2, stepIncrement = 0.1f, scene = UI_Scene.Editor, affectSymCounterparts = UI_Scene.All)]//Active Radar Range + public float ChaffEffectivity = 1; + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_SteerLimiter"), UI_FloatRange(minValue = .1f, maxValue = 1f, stepIncrement = .05f, scene = UI_Scene.Editor, affectSymCounterparts = UI_Scene.All)]//Steer Limiter public float MaxSteer = 1; @@ -146,6 +151,16 @@ private void RefreshGuidanceMode() GuidanceMode = GuidanceModes.AGMBallistic; GuidanceLabel = "Ballistic"; break; + + case 5: + GuidanceMode = GuidanceModes.PN; + GuidanceLabel = "Proportional Navigation"; + break; + + case 6: + GuidanceMode = GuidanceModes.APN; + GuidanceLabel = "Augmented Pro-Nav"; + break; } if (Fields["CruiseAltitude"] != null) @@ -172,15 +187,101 @@ private void RefreshGuidanceMode() Fields["SoftAscent"].guiActive = GuidanceMode == GuidanceModes.AGMBallistic; Fields["SoftAscent"].guiActiveEditor = GuidanceMode == GuidanceModes.AGMBallistic; } - Utils.RefreshAssociatedWindows(part); + + if (GuidanceMode != GuidanceModes.AAMLoft) + { + Fields["LoftMaxAltitude"].guiActive = false; + Fields["LoftMaxAltitude"].guiActiveEditor = false; + Fields["LoftRangeOverride"].guiActive = false; + Fields["LoftRangeOverride"].guiActiveEditor = false; + Fields["LoftAltitudeAdvMax"].guiActive = false; + Fields["LoftAltitudeAdvMax"].guiActiveEditor = false; + Fields["LoftMinAltitude"].guiActive = false; + Fields["LoftMinAltitude"].guiActiveEditor = false; + Fields["LoftAngle"].guiActive = false; + Fields["LoftAngle"].guiActiveEditor = false; + Fields["LoftTermAngle"].guiActive = false; + Fields["LoftTermAngle"].guiActiveEditor = false; + Fields["LoftRangeFac"].guiActive = false; + Fields["LoftRangeFac"].guiActiveEditor = false; + Fields["LoftVelComp"].guiActive = false; + Fields["LoftVelComp"].guiActiveEditor = false; + Fields["LoftVertVelComp"].guiActive = false; + Fields["LoftVertVelComp"].guiActiveEditor = false; + //Fields["LoftAltComp"].guiActive = false; + //Fields["LoftAltComp"].guiActiveEditor = false; + //Fields["terminalHomingRange"].guiActive = false; + //Fields["terminalHomingRange"].guiActiveEditor = false; + } + else + { + Fields["LoftMaxAltitude"].guiActive = true; + Fields["LoftMaxAltitude"].guiActiveEditor = true; + Fields["LoftRangeOverride"].guiActive = true; + Fields["LoftRangeOverride"].guiActiveEditor = true; + Fields["LoftAltitudeAdvMax"].guiActive = true; + Fields["LoftAltitudeAdvMax"].guiActiveEditor = true; + Fields["LoftMinAltitude"].guiActive = true; + Fields["LoftMinAltitude"].guiActiveEditor = true; + //Fields["terminalHomingRange"].guiActive = true; + //Fields["terminalHomingRange"].guiActiveEditor = true; + + if (!GameSettings.ADVANCED_TWEAKABLES) + { + Fields["LoftAngle"].guiActive = false; + Fields["LoftAngle"].guiActiveEditor = false; + Fields["LoftTermAngle"].guiActive = false; + Fields["LoftTermAngle"].guiActiveEditor = false; + Fields["LoftRangeFac"].guiActive = false; + Fields["LoftRangeFac"].guiActiveEditor = false; + Fields["LoftVelComp"].guiActive = false; + Fields["LoftVelComp"].guiActiveEditor = false; + Fields["LoftVertVelComp"].guiActive = false; + Fields["LoftVertVelComp"].guiActiveEditor = false; + //Fields["LoftAltComp"].guiActive = false; + //Fields["LoftAltComp"].guiActiveEditor = false; + } + else + { + Fields["LoftAngle"].guiActive = true; + Fields["LoftAngle"].guiActiveEditor = true; + Fields["LoftTermAngle"].guiActive = true; + Fields["LoftTermAngle"].guiActiveEditor = true; + Fields["LoftRangeFac"].guiActive = true; + Fields["LoftRangeFac"].guiActiveEditor = true; + Fields["LoftVelComp"].guiActive = true; + Fields["LoftVelComp"].guiActiveEditor = true; + Fields["LoftVertVelComp"].guiActive = true; + Fields["LoftVertVelComp"].guiActiveEditor = true; + //Fields["LoftAltComp"].guiActive = true; + //Fields["LoftAltComp"].guiActiveEditor = true; + } + } + + if (!terminalHoming && GuidanceMode != GuidanceModes.AAMLoft) //GuidanceMode != GuidanceModes.AAMHybrid && GuidanceMode != GuidanceModes.AAMLoft) + { + Fields["terminalHomingRange"].guiActive = false; + Fields["terminalHomingRange"].guiActiveEditor = false; + } + else + { + Fields["terminalHomingRange"].guiActive = true; + Fields["terminalHomingRange"].guiActiveEditor = true; + } + + GUIUtils.RefreshAssociatedWindows(part); } public override void OnFixedUpdate() { + base.OnFixedUpdate(); + + if (!HighLogic.LoadedSceneIsFlight) return; + if (HasFired && !HasExploded) { UpdateGuidance(); - CheckDetonationState(); + CheckDetonationState(true); CheckDetonationDistance(); CheckDelayedFired(); CheckNextStage(); @@ -199,8 +300,10 @@ public override void OnFixedUpdate() void Update() { + if (!HighLogic.LoadedSceneIsFlight) return; + if (!HasFired) - CheckDetonationState(); + CheckDetonationState(true); } private void CheckNextStage() @@ -317,7 +420,7 @@ private void MissileIgnition() MissileState = MissileStates.Cruise; _missileIgnited = true; - RadarWarningReceiver.WarnMissileLaunch(MissileReferenceTransform.position, GetForwardTransform()); + RadarWarningReceiver.WarnMissileLaunch(MissileReferenceTransform.position, GetForwardTransform(), TargetingMode == TargetingModes.Radar); } private bool ShouldExecuteNextStage() @@ -381,6 +484,7 @@ public override void OnStart(StartState state) { shortName = "Unnamed"; } + part.force_activate(); RefreshGuidanceMode(); @@ -392,9 +496,9 @@ public override void OnStart(StartState state) weaponClass = WeaponClasses.Missile; WeaponName = GetShortName(); - + if (HighLogic.LoadedSceneIsFlight) missileName = shortName; activeRadarRange = ActiveRadarRange; - + chaffEffectivity = ChaffEffectivity; //TODO: BDModularGuidance should be configurable? heatThreshold = 50; lockedSensorFOV = 5; @@ -406,12 +510,12 @@ public override void OnStart(StartState state) float a = lockedSensorFOV / 2f; float b = -1f * ((1f - 1f / 1.2f)); float[] x = new float[6] { 0f * a, 0.2f * a, 0.4f * a, 0.6f * a, 0.8f * a, 1f * a }; - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log("[BDArmory.BDModularGuidance]: OnStart missile " + shortName + ": setting default lockedSensorFOVBias curve to:"); + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log($"[BDArmory.BDModularGuidance]: OnStart missile {shortName}: setting default lockedSensorFOVBias curve to:"); for (int i = 0; i < 6; i++) { lockedSensorFOVBias.Add(x[i], b / (a * a) * x[i] * x[i] + 1f, -1f / 3f * x[i] / (a * a), -1f / 3f * x[i] / (a * a)); - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("key = " + x[i] + " " + (b / (a * a) * x[i] * x[i] + 1f) + " " + (-1f / 3f * x[i] / (a * a)) + " " + (-1f / 3f * x[i] / (a * a))); } } @@ -421,9 +525,9 @@ public override void OnStart(StartState state) { lockedSensorVelocityBias.Add(0f, 1f); lockedSensorVelocityBias.Add(180f, 1f); - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_MISSILES) { - Debug.Log("[BDArmory.BDModularGuidance]: OnStart missile " + shortName + ": setting default lockedSensorVelocityBias curve to:"); + Debug.Log($"[BDArmory.BDModularGuidance]: OnStart missile {shortName}: setting default lockedSensorVelocityBias curve to:"); Debug.Log("key = 0 1"); Debug.Log("key = 180 1"); } @@ -434,12 +538,17 @@ public override void OnStart(StartState state) { activeRadarLockTrackCurve.Add(0f, 0f); activeRadarLockTrackCurve.Add(activeRadarRange, RadarUtils.MISSILE_DEFAULT_LOCKABLE_RCS); // TODO: tune & balance constants! - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log("[BDArmory.BDModularGuidance]: OnStart missile " + shortName + ": setting default locktrackcurve with maxrange/minrcs: " + activeRadarLockTrackCurve.maxTime + "/" + RadarUtils.MISSILE_DEFAULT_LOCKABLE_RCS); + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log($"[BDArmory.BDModularGuidance]: OnStart missile {shortName}: setting default locktrackcurve with maxrange/minrcs: {activeRadarLockTrackCurve.maxTime} / {RadarUtils.MISSILE_DEFAULT_LOCKABLE_RCS}"); } - foreach (var explosivePart in VesselModuleRegistry.GetModules(vessel)) + + var explosiveParts = VesselModuleRegistry.GetModules(vessel); + if (explosiveParts != null) { - if (warheadYield < explosivePart.blastRadius) warheadYield = explosivePart.blastRadius; + foreach (var explosivePart in explosiveParts) + { + if (warheadYield < explosivePart.blastRadius) warheadYield = explosivePart.blastRadius; + } } } @@ -535,7 +644,7 @@ private void UpdateTargetingMode(TargetingModes newTargetingMode) TargetingMode = newTargetingMode; _targetingLabel = newTargetingMode.ToString(); - Utils.RefreshAssociatedWindows(part); + GUIUtils.RefreshAssociatedWindows(part); } private void OnDestroy() @@ -600,11 +709,17 @@ private Vector3 AAMGuidance() if (TargetAcquired) { float timeToImpact; - aamTarget = MissileGuidance.GetAirToAirTargetModular(TargetPosition, TargetVelocity, TargetAcceleration, vessel, out timeToImpact); + if (GuidanceIndex == 6) // Augmented Pro-Nav + aamTarget = MissileGuidance.GetAPNTarget(TargetPosition, TargetVelocity, TargetAcceleration, vessel, 3f, out timeToImpact); + else if (GuidanceIndex == 5) // Pro-Nav + aamTarget = MissileGuidance.GetPNTarget(TargetPosition, TargetVelocity, vessel, 3f, out timeToImpact); + else // AAM Lead + aamTarget = MissileGuidance.GetAirToAirTargetModular(TargetPosition, TargetVelocity, TargetAcceleration, vessel, out timeToImpact); TimeToImpact = timeToImpact; + if (Vector3.Angle(aamTarget - vessel.CoM, vessel.transform.forward) > maxOffBoresight * 0.75f) { - Debug.LogFormat("[BDArmory.BDModularGuidance]: Missile with Name={0} has exceeded the max off boresight, checking missed target ", vessel.vesselName); + if (BDArmorySettings.DEBUG_MISSILES) Debug.LogFormat("[BDArmory.BDModularGuidance]: Missile with Name={0} has exceeded the max off boresight, checking missed target ", vessel.vesselName); aamTarget = TargetPosition; } DrawDebugLine(vessel.CoM, aamTarget); @@ -628,7 +743,7 @@ private Vector3 AGMGuidance() if (targetViewAngle > maxOffBoresight) { - Debug.Log("[BDArmory.BDModularGuidance]: AGM Missile guidance failed - target out of view"); + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.BDModularGuidance]: AGM Missile guidance failed - target out of view"); guidanceActive = false; } } @@ -658,15 +773,13 @@ private Vector3 CruiseGuidance() private void CheckMiss(Vector3 targetPosition) { if (HasMissed) return; + if (MissileState != MissileStates.PostThrust) return; // if I'm to close to my vessel avoid explosion if ((vessel.CoM - SourceVessel.CoM).magnitude < 4 * DetonationDistance) return; - // if I'm getting closer to my target avoid explosion + // if I'm getting closer to my target avoid explosion if ((vessel.CoM - targetPosition).sqrMagnitude > (vessel.CoM + (vessel.Velocity() * Time.fixedDeltaTime) - (targetPosition + (TargetVelocity * Time.fixedDeltaTime))).sqrMagnitude) return; - - if (MissileState != MissileStates.PostThrust) return; - - Debug.Log("[BDArmory.BDModularGuidance]: Missile CheckMiss showed miss for " + vessel.vesselName + " with target at " + (targetPosition - vessel.CoM).ToString("0.0")); + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.BDModularGuidance]: Missile CheckMiss showed miss for {vessel.vesselName} ({SourceVessel}) with target at {targetPosition - vessel.CoM:G3}"); var pilotAI = VesselModuleRegistry.GetModule(vessel); // Get the pilot AI if the missile has one. if (pilotAI != null) @@ -681,11 +794,12 @@ private void CheckMiss(Vector3 targetPosition) TargetMf = null; isTimed = true; detonationTime = TimeIndex + 1.5f; + if (BDArmorySettings.CAMERA_SWITCH_INCLUDE_MISSILES && vessel.isActiveVessel) LoadedVesselSwitcher.Instance.TriggerSwitchVessel(); } private void ResetMissile() { - Debug.Log("[BDArmory.BDModularGuidance]: Resetting missile " + vessel.vesselName); + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.BDModularGuidance]: Resetting missile {vessel.vesselName}"); heatTarget = TargetSignatureData.noTarget; vrd = null; radarTarget = TargetSignatureData.noTarget; @@ -705,7 +819,7 @@ private void ResetMissile() MissileState = MissileStates.Idle; if (mfChecked && weaponManager != null) { - Debug.Log("[BDArmory.BDModularGuidance]: disabling target lock for " + vessel.vesselName); + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.BDModularGuidance]: disabling target lock for {vessel.vesselName}"); weaponManager.guardFiringMissile = false; // Disable target lock. mfChecked = false; } @@ -717,7 +831,7 @@ private void CheckMiss() if (MissileState == MissileStates.PostThrust && (vessel.LandedOrSplashed || vessel.Velocity().magnitude < 10f)) { - Debug.Log("[BDArmory.BDModularGuidance]: Missile CheckMiss showed miss for " + vessel.vesselName); + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.BDModularGuidance]: Missile CheckMiss showed miss for {vessel.vesselName}"); var pilotAI = VesselModuleRegistry.GetModule(vessel); // Get the pilot AI if the missile has one. if (pilotAI != null) @@ -732,12 +846,14 @@ private void CheckMiss() TargetMf = null; isTimed = true; detonationTime = TimeIndex + 1.5f; + if (BDArmorySettings.CAMERA_SWITCH_INCLUDE_MISSILES && vessel.isActiveVessel) LoadedVesselSwitcher.Instance.TriggerSwitchVessel(); } } public void GuidanceSteer(FlightCtrlState s) { + FloatingOriginCorrection(); debugString.Length = 0; if (guidanceActive && MissileReferenceTransform != null && _velocityTransform != null) { @@ -748,7 +864,7 @@ public void GuidanceSteer(FlightCtrlState s) } if (mfChecked && weaponManager != null && !weaponManager.guardFiringMissile) { - Debug.Log("[BDArmory.BDModularGuidance]: enabling target lock for " + vessel.vesselName); + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.BDModularGuidance]: enabling target lock for {vessel.vesselName}"); weaponManager.guardFiringMissile = true; // Enable target lock. } @@ -783,6 +899,12 @@ public void GuidanceSteer(FlightCtrlState s) case 4: newTargetPosition = BallisticGuidance(); break; + case 5: + newTargetPosition = AAMGuidance(); + break; + case 6: + newTargetPosition = AAMGuidance(); + break; } CheckMiss(newTargetPosition); @@ -821,14 +943,11 @@ private void SetRoll() var currentAngle = Vector3.SignedAngle(rollVessel, gravityVector, Vector3.Cross(rollVessel, gravityVector)) - 90f; - debugString.AppendLine($"Roll angle: {currentAngle}"); this.angularVelocity = currentAngle - this.lastRollAngle; //this.angularAcceleration = angularVelocity - this.lasAngularVelocity; var futureAngle = currentAngle + angularVelocity / Time.fixedDeltaTime * 1f; - debugString.AppendLine($"future Roll angle: {futureAngle}"); - if (futureAngle > 0.5f || currentAngle > 0.5f) { this.Roll = Mathf.Clamp(Roll - 0.001f, -1f, 0f); @@ -837,8 +956,13 @@ private void SetRoll() { this.Roll = Mathf.Clamp(Roll + 0.001f, 0, 1f); } - debugString.AppendLine($"Roll value: {this.Roll}"); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_MISSILES) + { + debugString.AppendLine($"Roll angle: {currentAngle}"); + debugString.AppendLine($"future Roll angle: {futureAngle}"); + debugString.AppendLine($"Roll value: {this.Roll}"); + } lastRollAngle = currentAngle; //lasAngularVelocity = angularVelocity; } @@ -874,7 +998,7 @@ private void OnActionGroupEditorClosed() /// /// /// - private PartModule FindFirstDecoupler(Part parent, PartModule last) + public static PartModule FindFirstDecoupler(Part parent, PartModule last) { if (parent == null || !parent) return last; @@ -896,7 +1020,7 @@ private PartModule FindFirstDecoupler(Part parent, PartModule last) /// public void ExecuteNextStage() { - Debug.LogFormat("[BDArmory.BDModularGuidance]: Executing next stage {0} for {1}", _nextStage, vessel.vesselName); + if (BDArmorySettings.DEBUG_MISSILES) Debug.LogFormat("[BDArmory.BDModularGuidance]: Executing next stage {0} for {1}", _nextStage, vessel.vesselName); vessel.ActionGroups.ToggleGroup( (KSPActionGroup)Enum.Parse(typeof(KSPActionGroup), "Custom0" + (int)_nextStage)); @@ -907,8 +1031,9 @@ public void ExecuteNextStage() //todo: find a way to fly by wire vessel decoupled } - void OnGUI() + protected override void OnGUI() { + base.OnGUI(); if (HighLogic.LoadedSceneIsFlight) { drawLabels(); @@ -984,10 +1109,15 @@ public override void FireMissile() guidanceActive = true; MissileState = MissileStates.Drop; - Utils.RefreshAssociatedWindows(part); + GUIUtils.RefreshAssociatedWindows(part); HasFired = true; DetonationDistanceState = DetonationDistanceStates.NotSafe; + if (vessel.atmDensity < 0.05) + { + vessel.ActionGroups.SetGroup(KSPActionGroup.RCS, true); + } + if (BDArmorySettings.CAMERA_SWITCH_INCLUDE_MISSILES && SourceVessel.isActiveVessel) LoadedVesselSwitcher.Instance.ForceSwitchVessel(vessel); } if (BDArmorySetup.Instance.ActiveWeaponManager != null) { @@ -1026,7 +1156,7 @@ void OnDisable() public void SwitchGuidanceMode() { GuidanceIndex++; - if (GuidanceIndex > 4) + if (GuidanceIndex > 6) { GuidanceIndex = 1; } @@ -1108,6 +1238,7 @@ public override void Detonate() { if (HasExploded || !HasFired) return; if (SourceVessel == null) SourceVessel = vessel; + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.BDModularGuidance]: Detonating missile {vessel.vesselName} ({SourceVessel})"); if (StageToTriggerOnProximity != 0) { @@ -1116,12 +1247,16 @@ public override void Detonate() } else { - foreach (var explosivePart in VesselModuleRegistry.GetModules(vessel)) - { if (!explosivePart.manualOverride) explosivePart.DetonateIfPossible(); } - if (VesselModuleRegistry.GetModules(vessel).Any(explosivePart => explosivePart.hasDetonated)) + var explosiveParts = VesselModuleRegistry.GetModules(vessel); + if (explosiveParts != null) { - HasExploded = true; - AutoDestruction(); + foreach (var explosivePart in explosiveParts) + { if (!explosivePart.manualOverride) explosivePart.DetonateIfPossible(); } + if (explosiveParts.Any(explosivePart => explosivePart.hasDetonated)) + { + HasExploded = true; + AutoDestruction(); + } } } } @@ -1206,7 +1341,7 @@ public class WeaponNameWindow : MonoBehaviour private Vector2 scrollPos; - [KSPField(isPersistant = false, guiActiveEditor = true, guiActive = false, guiName = "#LOC_BDArmory_RollCorrection_showRFGUI"), UI_Toggle(enabledText = "#LOC_BDArmory_showRFGUI_enabledText", disabledText = "#LOC_BDArmory_showRFGUI_disabledText")] [NonSerialized] public bool showRFGUI;//Show Weapon Name Editor--Weapon Name GUI--GUI + [KSPField(isPersistant = false, guiActiveEditor = true, guiActive = false, guiName = "#LOC_BDArmory_RollCorrection_showRFGUI"), UI_Toggle(enabledText = "#LOC_BDArmory_showRFGUI_enabledText", disabledText = "#LOC_BDArmory_showRFGUI_disabledText")][NonSerialized] public bool showRFGUI;//Show Weapon Name Editor--Weapon Name GUI--GUI private bool styleSetup; @@ -1344,7 +1479,7 @@ public void OnGUI() { editor.Unlock("BD_MN_GUILock"); } - guiWindowRect = GUILayout.Window(GetInstanceID(), guiWindowRect, GUIWindow, "Weapon Name GUI", Styles.styleEditorPanel); + guiWindowRect = GUILayout.Window(GUIUtility.GetControlID(FocusType.Passive), guiWindowRect, GUIWindow, "Weapon Name GUI", Styles.styleEditorPanel); } public void GUIWindow(int windowID) @@ -1376,7 +1511,7 @@ public void GUIWindow(int windowID) GUILayout.EndVertical(); GUI.DragWindow(); - BDGUIUtils.RepositionWindow(ref guiWindowRect); + GUIUtils.RepositionWindow(ref guiWindowRect); } private static void InitializeStyles() diff --git a/BDArmory/Modules/MissileBase.cs b/BDArmory/Weapons/Missiles/MissileBase.cs similarity index 60% rename from BDArmory/Modules/MissileBase.cs rename to BDArmory/Weapons/Missiles/MissileBase.cs index f07a66122..b471aee5e 100644 --- a/BDArmory/Modules/MissileBase.cs +++ b/BDArmory/Weapons/Missiles/MissileBase.cs @@ -2,20 +2,21 @@ using System.Collections.Generic; using System.Linq; using System.Text; -using BDArmory.Core; -using BDArmory.Core.Extension; -using BDArmory.Core.Utils; -using BDArmory.CounterMeasure; +using UnityEngine; + +using BDArmory.Competition; using BDArmory.Control; +using BDArmory.CounterMeasure; +using BDArmory.Extensions; using BDArmory.FX; using BDArmory.Guidances; -using BDArmory.Misc; using BDArmory.Radar; +using BDArmory.Settings; using BDArmory.Targeting; using BDArmory.UI; -using UnityEngine; +using BDArmory.Utils; -namespace BDArmory.Modules +namespace BDArmory.Weapons.Missiles { public abstract class MissileBase : EngageableWeapon, IBDWeapon { @@ -40,6 +41,23 @@ public string GetMissileType() return missileType; } + public string GetPartName() + { + return missileName; + } + + public float GetEngageRange() + { + return GetEngagementRangeMax(); + } + + public string missileName { get; set; } = ""; + + [KSPField(isPersistant = false, guiActive = true, guiName = "Launched from"), UI_Label(scene = UI_Scene.Flight)] + public string SourceVesselName; + [KSPField(isPersistant = false, guiActive = true, guiName = "Launched at"), UI_Label(scene = UI_Scene.Flight)] + public string TargetVesselName; + [KSPField] public string missileType = "missile"; @@ -49,6 +67,8 @@ public string GetMissileType() [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_MinStaticLaunchRange"), UI_FloatRange(minValue = 10f, maxValue = 4000f, stepIncrement = 100f, scene = UI_Scene.Editor, affectSymCounterparts = UI_Scene.All)]//Min Static Launch Range public float minStaticLaunchRange = 10; + public float StandOffDistance = -1; + [KSPField] public float minLaunchSpeed = 0; @@ -78,9 +98,20 @@ public float getSWLWOffset } } + [KSPField] + public float engineFailureRate = 0f; // How often the missile engine will fail to start (0-1), evaluated once on missile launch + + [KSPField] + public float guidanceFailureRate = 0f; // Probability the missile guidance will fail per second (0-1), evaluated every frame after launch + + public float guidanceFailureRatePerFrame = 0f; // guidanceFailureRate (per second) converted to per frame probability + [KSPField] public bool guidanceActive = true; + [KSPField] + public float gpsUpdates = -1f; // GPS missiles get updates on target position from source vessel every gpsUpdates >= 0 seconds + [KSPField] public float lockedSensorFOV = 2.5f; @@ -94,13 +125,25 @@ public float getSWLWOffset public float heatThreshold = 150; [KSPField] - public bool allAspect = false; + public float frontAspectHeatModifier = 1f; // Modifies heat value returned to missiles outside of ~50 deg exhaust cone from non-prop engines. Only takes affect when ASPECTED_IR_SEEKERS = true in settings.cfg + + [KSPField] + public float chaffEffectivity = 1f; // Modifies how the missile targeting is affected by chaff, 1 is fully affected (normal behavior), lower values mean less affected (0 is ignores chaff), higher values means more affected + + [KSPField] + public bool allAspect = false; // DEPRECATED, replaced by uncagedIRLock. uncagedIRLock is automatically set to this value upon loading (to maintain compatability with old BDA mods) + + [KSPField] + public bool uncagedLock = false; //if true it simulates a modern IR missile with "uncaged lock" ability. Even if the target is not within boresight fov, it can be radar locked and the target information transfered to the missile. It will then try to lock on with the heat seeker. If false, it is an older missile which requires a direct "in boresight" lock. [KSPField] public bool isTimed = false; [KSPField] - public bool radarLOAL = false; + public bool radarLOAL = false; //if true, radar missile will acquire and lock onto a target after launch, using the missile's onboard radar + + [KSPField] + public bool canRelock = true; //if true, if a FCS radar guiding a SARH missile loses lock, the missile will be switched to the active radar lock instead of going inactive from target loss. [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_DropTime"),//Drop Time UI_FloatRange(minValue = 0f, maxValue = 5f, stepIncrement = 0.5f, scene = UI_Scene.Editor)] @@ -185,6 +228,45 @@ public float getSWLWOffset [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_CruisePredictionTime"), UI_FloatRange(minValue = 1f, maxValue = 15f, stepIncrement = 1f, scene = UI_Scene.Editor, affectSymCounterparts = UI_Scene.All)]//Cruise prediction time public float CruisePredictionTime = 5; + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_LoftMaxAltitude"), UI_FloatRange(minValue = 5000f, maxValue = 30000f, stepIncrement = 100f, scene = UI_Scene.Editor, affectSymCounterparts = UI_Scene.All)]//Loft Max Altitude + public float LoftMaxAltitude = 16000; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_LoftRangeOverride"), UI_FloatRange(minValue = 500f, maxValue = 25000f, stepIncrement = 100f, scene = UI_Scene.Editor, affectSymCounterparts = UI_Scene.All)]//Loft Altitude Difference + public float LoftRangeOverride = 15000; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_LoftAltitudeAdvMax"), UI_FloatRange(minValue = 500f, maxValue = 5000f, stepIncrement = 100f, scene = UI_Scene.Editor, affectSymCounterparts = UI_Scene.All)]//Loft Maximum Altitude Advantage + public float LoftAltitudeAdvMax = 3000; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_LoftMinAltitude"), UI_FloatRange(minValue = 0f, maxValue = 10000f, stepIncrement = 100f, scene = UI_Scene.Editor, affectSymCounterparts = UI_Scene.All)]//Loft Maximum Altitude Advantage + public float LoftMinAltitude = 6000; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_LoftAngle"), UI_FloatRange(minValue = 0f, maxValue = 90f, stepIncrement = 0.5f, scene = UI_Scene.Editor, affectSymCounterparts = UI_Scene.All)]//Loft Angle + public float LoftAngle = 45; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_LoftTermAngle"), UI_FloatRange(minValue = 0f, maxValue = 90f, stepIncrement = 0.5f, scene = UI_Scene.Editor, affectSymCounterparts = UI_Scene.All)]//Loft Termination Angle + public float LoftTermAngle = 20; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_LoftRangeFac"),//Loft Range Factor + UI_FloatRange(minValue = 0.1f, maxValue = 5.0f, stepIncrement = 0.01f, scene = UI_Scene.Editor)] + public float LoftRangeFac = 0.5f; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_LoftVelComp"),//Loft Velocity Compensation (Horizontal) + UI_FloatRange(minValue = -2.0f, maxValue = 2.0f, stepIncrement = 0.01f, scene = UI_Scene.Editor)] + public float LoftVelComp = -0.5f; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_LoftVertVelComp"),//Loft Velocity Compensation (Vertical) + UI_FloatRange(minValue = -2.0f, maxValue = 2.0f, stepIncrement = 0.01f, scene = UI_Scene.Editor)] + public float LoftVertVelComp = -0.5f; + + //[KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_LoftAltComp"), UI_FloatRange(minValue = -2000f, maxValue = 2000f, stepIncrement = 10f, scene = UI_Scene.Editor, affectSymCounterparts = UI_Scene.All)]//Loft Altitude Compensation + //public float LoftAltComp = 0; + + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_terminalHomingRange"), UI_FloatRange(minValue = 500f, maxValue = 20000f, stepIncrement = 100f, scene = UI_Scene.Editor, affectSymCounterparts = UI_Scene.All)]//Terminal Homing Range + public float terminalHomingRange = 3000; + + [KSPField] + public bool terminalHoming = false; + [KSPField] public float missileRadarCrossSection = RadarUtils.RCS_MISSILES; // radar cross section of this missile for detection purposes @@ -198,13 +280,18 @@ public enum TargetingModes { None, Radar, Heat, Laser, Gps, AntiRad } public DetonationDistanceStates DetonationDistanceState { get; set; } = DetonationDistanceStates.NotSafe; - public enum GuidanceModes { None, AAMLead, AAMPure, AGM, AGMBallistic, Cruise, STS, Bomb, RCS, BeamRiding, SLW } + public enum GuidanceModes { None, AAMLead, AAMPure, AGM, AGMBallistic, Cruise, STS, Bomb, RCS, BeamRiding, SLW, PN, APN, AAMLoft } public GuidanceModes GuidanceMode; + public enum WarheadTypes { Standard, ContinuousRod, EMP, Nuke } + + public WarheadTypes warheadType; public bool HasFired { get; set; } = false; - public BDTeam Team { get; set; } + public bool launched = false; + + public BDTeam Team { get; set; } = BDTeam.Get("Neutral"); public bool HasMissed { get; set; } = false; @@ -220,24 +307,40 @@ public enum GuidanceModes { None, AAMLead, AAMPure, AGM, AGMBallistic, Cruise, S public TargetingModes TargetingModeTerminal { get; set; } + public GuidanceModes homingModeTerminal { get; set; } + + public bool terminalHomingActive = false; + public float TimeToImpact { get; set; } public bool TargetAcquired { get; set; } public bool ActiveRadar { get; set; } - public Vessel SourceVessel { get; set; } = null; + public Vessel SourceVessel + { + get { return _sourceVessel; } + set + { + _sourceVessel = value; + SourceVesselName = SourceVessel != null ? SourceVessel.vesselName : ""; + } + } + Vessel _sourceVessel = null; public bool HasExploded { get; set; } = false; - public int clusterbomb { get; set; } = 1; + public bool FuseFailed { get; set; } = false; - public bool EMP { get; set; } = false; + public bool HasDied { get; set; } = false; + + public int clusterbomb { get; set; } = 1; protected IGuidance _guidance; private double _lastVerticalSpeed; private double _lastHorizontalSpeed; + private int gpsUpdateCounter = 0; public double HorizontalAcceleration { @@ -279,7 +382,17 @@ public float Throttle protected float lockFailTimer = -1; - public Vessel legacyTargetVessel; + public TargetInfo targetVessel + { + get { return _targetVessel; } + set + { + _targetVessel = value; + if (_targetVessel != null && _targetVessel.Vessel != null) + TargetVesselName = _targetVessel.Vessel.vesselName; + } + } + TargetInfo _targetVessel; public Transform MissileReferenceTransform; @@ -308,35 +421,78 @@ public float Throttle private int snapshotTicker; private int locksCount = 0; private float _radarFailTimer = 0; - private float maxRadarFailTime = 5; + + [KSPField] public float radarTimeout = 5; private float lastRWRPing = 0; private bool radarLOALSearching = false; + private bool hasLostLock = false; protected bool checkMiss = false; public StringBuilder debugString = new StringBuilder(); private float _throttle = 1f; - Vector3 previousPos; public string Sublabel; public int missilecount = 0; //#191 + RaycastHit[] proximityHits = new RaycastHit[100]; + Collider[] proximityHitColliders = new Collider[100]; + int layerMask = (int)(LayerMasks.Parts | LayerMasks.Scenery | LayerMasks.Unknown19 | LayerMasks.Wheels); + + /// + /// Make corrections for floating origin and Krakensbane adjustments. + /// This can't simply be in OnFixedUpdate as it needs to be called differently for MissileLauncher (which uses OnFixedUpdate) and BDModularGuidance (which uses FlyByWire which triggers before OnFixedUpdate). + /// + public void FloatingOriginCorrection() + { + if (HasFired && !HasExploded) + { + if (BDKrakensbane.IsActive) + { + // Debug.Log($"DEBUG {Time.time} Correcting for floating origin shift of {(Vector3)BDKrakensbane.FloatingOriginOffset:G3} ({(Vector3)BDKrakensbane.FloatingOriginOffsetNonKrakensbane:G3}) for {vessel.vesselName} ({SourceVessel})"); + TargetPosition -= BDKrakensbane.FloatingOriginOffsetNonKrakensbane; + } + } + } + + public ModuleMissileRearm reloadableRail = null; + public bool hasAmmo = false; + int AmmoCount // Returns the ammo count if the part contains ModuleMissileRearm, otherwise 1. + { + get + { + if (!hasAmmo) return 1; + return (int)reloadableRail.ammoCount; + } + } + + public override void OnAwake() + { + base.OnAwake(); + var MMG = GetPart().FindModuleImplementing(); + if (MMG == null) + { + hasAmmo = false; + } + } public void GetMissileCount() // could stick this in GetSublabel, but that gets called every frame by BDArmorySetup? { missilecount = 0; - using (List.Enumerator craftPart = vessel.parts.GetEnumerator()) + if (part is null) return; + var missilePartName = GetPartName(); + if (string.IsNullOrEmpty(missilePartName)) return; + using (var craftPart = VesselModuleRegistry.GetMissileBases(vessel).GetEnumerator()) while (craftPart.MoveNext()) { - if (craftPart.Current == null) continue; - if (part == null) continue; - if (part.name == null) continue; - if (craftPart.Current.name != part.name) continue; - missilecount++; + if (craftPart.Current is null) continue; + if (craftPart.Current.GetPartName() != missilePartName) continue; + if (craftPart.Current.engageRangeMax != engageRangeMax) continue; + missilecount += craftPart.Current.AmmoCount; } } public string GetSubLabel() { - return Sublabel = "Guidance: " + Enum.GetName(typeof(TargetingModes), TargetingMode) + "; Remaining: " + missilecount; // + return Sublabel = $"Guidance: {Enum.GetName(typeof(TargetingModes), TargetingMode)}; Max Range: {Mathf.Round(engageRangeMax / 100) / 10} km; Remaining: {missilecount}"; } public Part GetPart() @@ -428,6 +584,24 @@ public Vector3d UpdateGPSTarget() else { gpsTargetCoords_ = targetGPSCoords; + if (targetVessel && HasFired && (gpsUpdates >= 0f) && VesselModuleRegistry.GetMissileFire(SourceVessel).CanSeeTarget(targetVessel)) + { + if (gpsUpdates == 0) // Constant updates + { + gpsTargetCoords_ = VectorUtils.WorldPositionToGeoCoords(targetVessel.Vessel.CoM, targetVessel.Vessel.mainBody); + targetGPSCoords = gpsTargetCoords_; + } + else // Update every gpsUpdates seconds + { + float updateCount = TimeIndex / gpsUpdates; + if (updateCount > gpsUpdateCounter) + { + gpsUpdateCounter++; + gpsTargetCoords_ = VectorUtils.WorldPositionToGeoCoords(targetVessel.Vessel.CoM, targetVessel.Vessel.mainBody); + targetGPSCoords = gpsTargetCoords_; + } + } + } } if (TargetAcquired) @@ -449,7 +623,7 @@ protected void UpdateHeatTarget() if (lockFailTimer > 1) { - legacyTargetVessel = null; + targetVessel = null; TargetAcquired = false; predictedHeatTarget.exists = false; predictedHeatTarget.signalStrength = 0; @@ -483,11 +657,10 @@ protected void UpdateHeatTarget() if (offBoresightAngle > maxOffBoresight) lookRay = new Ray(lookRay.origin, Vector3.RotateTowards(lookRay.direction, GetForwardTransform(), (offBoresightAngle - maxOffBoresight) * Mathf.Deg2Rad, 0)); - if (BDArmorySettings.DRAW_DEBUG_LINES) - DrawDebugLine(lookRay.origin, lookRay.origin + lookRay.direction * 10000, Color.magenta); + DrawDebugLine(lookRay.origin, lookRay.origin + lookRay.direction * 10000, Color.magenta); // Update heat target - heatTarget = BDATargetManager.GetHeatTarget(SourceVessel, vessel, lookRay, predictedHeatTarget, lockedSensorFOV / 2, heatThreshold, allAspect, lockedSensorFOVBias, lockedSensorVelocityBias, (SourceVessel == null ? null : SourceVessel.gameObject == null ? null : SourceVessel.gameObject.GetComponent())); + heatTarget = BDATargetManager.GetHeatTarget(SourceVessel, vessel, lookRay, predictedHeatTarget, lockedSensorFOV / 2, heatThreshold, frontAspectHeatModifier, uncagedLock, lockedSensorFOVBias, lockedSensorVelocityBias, (SourceVessel == null ? null : SourceVessel.gameObject == null ? null : SourceVessel.gameObject.GetComponent()), targetVessel); if (heatTarget.exists) { @@ -502,11 +675,7 @@ protected void UpdateHeatTarget() } else { - TargetAcquired = false; - if (FlightGlobals.ready) - { - lockFailTimer += Time.fixedDeltaTime; - } + lockFailTimer += Time.fixedDeltaTime; } // Update predicted values based on target information @@ -566,7 +735,7 @@ protected void UpdateLaserTarget() { if (CMSmoke.RaycastSmoke(new Ray(transform.position, lastLaserPoint - transform.position))) { - //Debug.Log("Laser missileBase affected by smoke countermeasure"); + //Debug.Log("[BDArmory.MissileBase]: Laser missileBase affected by smoke countermeasure"); float angle = VectorUtils.FullRangePerlinNoise(0.75f * Time.time, 10) * BDArmorySettings.SMOKE_DEFLECTION_FACTOR; TargetPosition = VectorUtils.RotatePointAround(lastLaserPoint, transform.position, VectorUtils.GetUpDirection(transform.position), angle); TargetVelocity = Vector3.zero; @@ -586,7 +755,7 @@ protected void UpdateLaserTarget() foundCam = BDATargetManager.GetLaserTarget(this, parentOnly); if (foundCam != null && foundCam.cameraEnabled && foundCam.groundStabilized && BDATargetManager.CanSeePosition(foundCam.groundTargetPosition, vessel.transform.position, MissileReferenceTransform.position)) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.MissileBase]: Laser guided missileBase actively found laser point. Enabling guidance."); + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.MissileBase]: Laser guided missileBase actively found laser point. Enabling guidance."); lockedCamera = foundCam; TargetAcquired = true; } @@ -602,30 +771,38 @@ protected void UpdateRadarTarget() if (radarTarget.exists) { // locked-on before launch, passive radar guidance or waiting till in active radar range: - if (!ActiveRadar && ((radarTarget.predictedPosition - transform.position).sqrMagnitude > Mathf.Pow(activeRadarRange, 2) || angleToTarget > maxOffBoresight * 0.75f)) + if (!ActiveRadar && ((radarTarget.predictedPosition - transform.position).sqrMagnitude > (activeRadarRange * activeRadarRange) || angleToTarget > maxOffBoresight * 0.75f)) { if (vrd) { TargetSignatureData t = TargetSignatureData.noTarget; - List possibleTargets = vrd.GetLockedTargets(); - for (int i = 0; i < possibleTargets.Count; i++) + if (canRelock && hasLostLock) + { + if (vrd.locked) t = vrd.lockedTargetData.targetData; //SARH is passive, and guided towards whatever is currently painted by FCS radar + } + else { - if (possibleTargets[i].vessel == radarTarget.vessel) + List possibleTargets = vrd.GetLockedTargets(); + for (int i = 0; i < possibleTargets.Count; i++) { - t = possibleTargets[i]; + if (possibleTargets[i].vessel == radarTarget.vessel) //this means SARh will remain locked to whatever was the initial target, regardless of current radar lock + { + t = possibleTargets[i]; + } } } - if (t.exists) { TargetAcquired = true; + hasLostLock = false; radarTarget = t; + hasLostLock = false; if (weaponClass == WeaponClasses.SLW) { TargetPosition = radarTarget.predictedPosition; } else - TargetPosition = radarTarget.predictedPositionWithChaffFactor; + TargetPosition = radarTarget.predictedPositionWithChaffFactor(chaffEffectivity); TargetVelocity = radarTarget.velocity; TargetAcceleration = radarTarget.acceleration; _radarFailTimer = 0; @@ -633,18 +810,19 @@ protected void UpdateRadarTarget() } else { - if (_radarFailTimer > maxRadarFailTime) + if (_radarFailTimer > radarTimeout) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.MissileBase]: Semi-Active Radar guidance failed. Parent radar lost target."); + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.MissileBase]: Semi-Active Radar guidance failed. Parent radar lost target."); radarTarget = TargetSignatureData.noTarget; - legacyTargetVessel = null; + targetVessel = null; return; } else { if (_radarFailTimer == 0) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.MissileBase]: Semi-Active Radar guidance failed - waiting for data"); + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.MissileBase]: Semi-Active Radar guidance failed - waiting for data"); + hasLostLock = true; } _radarFailTimer += Time.fixedDeltaTime; radarTarget.timeAcquired = Time.time; @@ -654,7 +832,7 @@ protected void UpdateRadarTarget() TargetPosition = radarTarget.predictedPosition; } else - TargetPosition = radarTarget.predictedPositionWithChaffFactor; + TargetPosition = radarTarget.predictedPositionWithChaffFactor(chaffEffectivity); TargetVelocity = radarTarget.velocity; TargetAcceleration = Vector3.zero; TargetAcquired = true; @@ -663,22 +841,21 @@ protected void UpdateRadarTarget() } else { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.MissileBase]: Semi-Active Radar guidance failed. Out of range and no data feed."); + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.MissileBase]: Semi-Active Radar guidance failed. Out of range and no data feed."); radarTarget = TargetSignatureData.noTarget; - legacyTargetVessel = null; + targetVessel = null; return; } } - else + else //onboard radar is on, or off but in range { // active radar with target locked: vrd = null; - if (angleToTarget > maxOffBoresight) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.MissileBase]: Active Radar guidance failed. Target is out of active seeker gimbal limits."); + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.MissileBase]: Active Radar guidance failed. Target is out of active seeker gimbal limits."); radarTarget = TargetSignatureData.noTarget; - legacyTargetVessel = null; + targetVessel = null; return; } else @@ -701,11 +878,12 @@ protected void UpdateRadarTarget() //RadarUtils.UpdateRadarLock(ray, lockedSensorFOV, activeRadarMinThresh, ref scannedTargets, 0.4f, pingRWR, RadarWarningReceiver.RWRThreatTypes.MissileLock, radarSnapshot); RadarUtils.RadarUpdateMissileLock(ray, lockedSensorFOV, ref scannedTargets, 0.4f, this); - float sqrThresh = radarLOALSearching ? Mathf.Pow(500, 2) : Mathf.Pow(40, 2); + float sqrThresh = radarLOALSearching ? 250000f : 1600; // 500 * 500 : 40 * 40; if (radarLOAL && radarLOALSearching && !radarSnapshot) { //only scan on snapshot interval + TargetAcquired = true; } else { @@ -724,7 +902,7 @@ protected void UpdateRadarTarget() TargetPosition = radarTarget.predictedPosition + (radarTarget.velocity * Time.fixedDeltaTime); } else - TargetPosition = radarTarget.predictedPositionWithChaffFactor + (radarTarget.velocity * Time.fixedDeltaTime); + TargetPosition = radarTarget.predictedPositionWithChaffFactor(chaffEffectivity) + (radarTarget.velocity * Time.fixedDeltaTime); TargetVelocity = radarTarget.velocity; TargetAcceleration = radarTarget.acceleration; @@ -737,13 +915,13 @@ protected void UpdateRadarTarget() RadarWarningReceiver.PingRWR(ray, lockedSensorFOV, RadarWarningReceiver.RWRThreatTypes.Torpedo, 2f); else RadarWarningReceiver.PingRWR(ray, lockedSensorFOV, RadarWarningReceiver.RWRThreatTypes.MissileLaunch, 2f); - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.MissileBase]: Pitbull! Radar missilebase has gone active. Radar sig strength: " + radarTarget.signalStrength.ToString("0.0")); + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileBase]: Pitbull! Radar missilebase has gone active. Radar sig strength: {radarTarget.signalStrength:0.0}"); } else if (locksCount > 2) { guidanceActive = false; checkMiss = true; - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_MISSILES) { Debug.Log("[BDArmory.MissileBase]: Active Radar guidance failed. Radar missileBase reached max re-lock attempts."); } @@ -766,7 +944,7 @@ protected void UpdateRadarTarget() TargetPosition = radarTarget.predictedPosition + (radarTarget.velocity * Time.fixedDeltaTime); } else - TargetPosition = radarTarget.predictedPositionWithChaffFactor + (radarTarget.velocity * Time.fixedDeltaTime); + TargetPosition = radarTarget.predictedPositionWithChaffFactor(chaffEffectivity) + (radarTarget.velocity * Time.fixedDeltaTime); TargetVelocity = radarTarget.velocity; TargetAcceleration = Vector3.zero; @@ -775,17 +953,18 @@ protected void UpdateRadarTarget() } else { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.MissileBase]: Active Radar guidance failed. No target locked."); + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.MissileBase]: Active Radar guidance failed. No target locked."); radarTarget = TargetSignatureData.noTarget; - legacyTargetVessel = null; + targetVessel = null; radarLOALSearching = false; + radarLOAL = false; TargetAcquired = false; ActiveRadar = false; } } } } - else if (radarLOAL && radarLOALSearching) + else if (radarLOAL && radarLOALSearching) //add a check for missing radar, so LOAL missiles that have been dumbfired can still activate? { // not locked on before launch, trying lock-on after launch: @@ -807,18 +986,19 @@ protected void UpdateRadarTarget() //RadarUtils.UpdateRadarLock(ray, lockedSensorFOV * 3, activeRadarMinThresh * 2, ref scannedTargets, 0.4f, pingRWR, RadarWarningReceiver.RWRThreatTypes.MissileLock, radarSnapshot); RadarUtils.RadarUpdateMissileLock(ray, lockedSensorFOV * 3, ref scannedTargets, 0.4f, this); - float sqrThresh = Mathf.Pow(300, 2); + float sqrThresh = targetVessel != null ? 1000000 : 90000f; // 1000 * 1000 : 300 * 300; Expand threshold if no target to search for, grab first available target - float smallestAngle = 360; + float smallestAngle = maxOffBoresight; TargetSignatureData lockedTarget = TargetSignatureData.noTarget; - + Vector3 soughtTarget = radarTarget.exists ? radarTarget.predictedPosition : targetVessel != null ? targetVessel.Vessel.CoM : transform.position + (startDirection); for (int i = 0; i < scannedTargets.Length; i++) { - if (scannedTargets[i].exists && (scannedTargets[i].predictedPosition - radarTarget.predictedPosition).sqrMagnitude < sqrThresh) + if (scannedTargets[i].exists && (scannedTargets[i].predictedPosition - soughtTarget).sqrMagnitude < sqrThresh) { //re-check engagement envelope, only lock appropriate targets if (CheckTargetEngagementEnvelope(scannedTargets[i].targetInfo)) { + if (scannedTargets[i].targetInfo.Team == Team) continue;//Don't lock friendlies float angle = Vector3.Angle(scannedTargets[i].predictedPosition - transform.position, GetForwardTransform()); if (angle < smallestAngle) { @@ -827,7 +1007,7 @@ protected void UpdateRadarTarget() } ActiveRadar = true; - return; + //return; } } } @@ -842,7 +1022,7 @@ protected void UpdateRadarTarget() TargetPosition = radarTarget.predictedPosition + (radarTarget.velocity * Time.fixedDeltaTime); } else - TargetPosition = radarTarget.predictedPositionWithChaffFactor + (radarTarget.velocity * Time.fixedDeltaTime); + TargetPosition = radarTarget.predictedPositionWithChaffFactor(chaffEffectivity) + (radarTarget.velocity * Time.fixedDeltaTime); TargetVelocity = radarTarget.velocity; TargetAcceleration = radarTarget.acceleration; @@ -853,23 +1033,24 @@ protected void UpdateRadarTarget() else RadarWarningReceiver.PingRWR(new Ray(transform.position, radarTarget.predictedPosition - transform.position), lockedSensorFOV, RadarWarningReceiver.RWRThreatTypes.MissileLaunch, 2f); - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.MissileBase]: Pitbull! Radar missileBase has gone active. Radar sig strength: " + radarTarget.signalStrength.ToString("0.0")); + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileBase]: Pitbull! Radar missileBase has gone active. Radar sig strength: {radarTarget.signalStrength:0.0}"); } return; } else { + radarTarget = TargetSignatureData.noTarget; TargetAcquired = true; TargetPosition = transform.position + (startDirection * 500); TargetVelocity = Vector3.zero; TargetAcceleration = Vector3.zero; radarLOALSearching = true; _radarFailTimer += Time.fixedDeltaTime; - if (_radarFailTimer > maxRadarFailTime) + if (_radarFailTimer > radarTimeout) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.MissileBase]: Active Radar guidance failed. LOAL could not lock a target."); - radarTarget = TargetSignatureData.noTarget; - legacyTargetVessel = null; + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.MissileBase]: Active Radar guidance failed. LOAL could not lock a target."); + radarLOAL = false; + targetVessel = null; radarLOALSearching = false; TargetAcquired = false; ActiveRadar = false; @@ -880,12 +1061,30 @@ protected void UpdateRadarTarget() if (!radarTarget.exists) { - legacyTargetVessel = null; + if (_radarFailTimer < radarTimeout) + { + if (vrd) + radarTarget = vrd.lockedTargetData.targetData; + else if (radarLOAL) + radarLOALSearching = true; + else + { + targetVessel = null; + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.MissileBase]: No assigned radar target. Awaiting timeout.... "); + } + } + else + { + targetVessel = null; + TargetAcquired = false; + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.MissileBase]: No radar target. Active Radar guidance timed out. "); + } } } protected bool CheckTargetEngagementEnvelope(TargetInfo ti) { + if (ti == null) return false; return (ti.isMissile && engageMissile) || (!ti.isMissile && ti.isFlying && engageAir) || ((ti.isLandedOrSurfaceSplashed || ti.isSplashed) && engageGround) || @@ -897,9 +1096,10 @@ protected void ReceiveRadarPing(Vessel v, Vector3 source, RadarWarningReceiver.R if (TargetingMode == TargetingModes.AntiRad && TargetAcquired && v == vessel) { // Ping was close to the previous target position and is within the boresight of the missile. - if ((source - VectorUtils.GetWorldSurfacePostion(targetGPSCoords, vessel.mainBody)).sqrMagnitude < Mathf.Pow(maxStaticLaunchRange / 4, 2) && Vector3.Angle(source - transform.position, GetForwardTransform()) < maxOffBoresight) + var staticLaunchThresholdSqr = maxStaticLaunchRange * maxStaticLaunchRange / 16f; + if ((source - VectorUtils.GetWorldSurfacePostion(targetGPSCoords, vessel.mainBody)).sqrMagnitude < staticLaunchThresholdSqr && Vector3.Angle(source - transform.position, GetForwardTransform()) < maxOffBoresight) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.MissileBase]: Radar ping! Adjusting target position by " + (source - VectorUtils.GetWorldSurfacePostion(targetGPSCoords, vessel.mainBody)).magnitude + " to " + TargetPosition); + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileBase]: Radar ping! Adjusting target position by {(source - VectorUtils.GetWorldSurfacePostion(targetGPSCoords, vessel.mainBody)).magnitude} to {TargetPosition}"); TargetAcquired = true; TargetPosition = source; targetGPSCoords = VectorUtils.WorldPositionToGeoCoords(TargetPosition, vessel.mainBody); @@ -912,35 +1112,25 @@ protected void ReceiveRadarPing(Vessel v, Vector3 source, RadarWarningReceiver.R protected void UpdateAntiRadiationTarget() { - if (!TargetAcquired) - { - guidanceActive = false; - return; - } - - if (FlightGlobals.ready) + if (FlightGlobals.ready && TargetAcquired) { if (lockFailTimer < 0) { lockFailTimer = 0; } lockFailTimer += Time.fixedDeltaTime; + if (lockFailTimer > 8) + { + TargetAcquired = false; + } } - - if (lockFailTimer > 8) - { - guidanceActive = false; - TargetAcquired = false; - } - else - { + if (targetGPSCoords != Vector3d.zero) TargetPosition = VectorUtils.GetWorldSurfacePostion(targetGPSCoords, vessel.mainBody); - } } public void DrawDebugLine(Vector3 start, Vector3 end, Color color = default(Color)) { - if (BDArmorySettings.DRAW_DEBUG_LINES) + if (BDArmorySettings.DEBUG_LINES) { if (!gameObject.GetComponent()) { @@ -952,17 +1142,23 @@ protected void UpdateAntiRadiationTarget() { LR = gameObject.GetComponent(); } + LR.enabled = true; LR.positionCount = 2; LR.SetPosition(0, start); LR.SetPosition(1, end); } } + protected virtual void OnGUI() + { + if (!BDArmorySettings.DEBUG_LINES && LR != null) { LR.enabled = false; } + } + protected void CheckDetonationDistance() { if (DetonationDistanceState == DetonationDistanceStates.Detonate) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.MissileBase]: Target detected inside sphere - detonating"); + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.MissileBase]: Target detected inside sphere - detonating"); Detonate(); } @@ -978,16 +1174,12 @@ protected Vector3 CalculateAGMBallisticGuidance(MissileBase missile, Vector3 tar return _guidance.GetDirection(this, targetPosition, Vector3.zero); } - - - - protected void drawLabels() { if (vessel == null || !HasFired || !vessel.isActiveVessel) return; - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_MISSILES) { - GUI.Label(new Rect(200, Screen.height - 300, 600, 300), this.shortName + "\n" + debugString.ToString()); + GUI.Label(new Rect(200, Screen.height - 300, 600, 300), $"{this.shortName}\n{debugString}"); } } @@ -996,11 +1188,10 @@ public float GetTntMass() return VesselModuleRegistry.GetModules(vessel).Max(x => x.tntMass); } - public void CheckDetonationState() + public void CheckDetonationState(bool separateWarheads = false) { //Guard clauses - if (!TargetAcquired) return; - + //if (!TargetAcquired) return; var targetDistancePerFrame = TargetVelocity * Time.fixedDeltaTime; var missileDistancePerFrame = vessel.Velocity() * Time.fixedDeltaTime; @@ -1012,145 +1203,178 @@ public void CheckDetonationState() switch (DetonationDistanceState) { case DetonationDistanceStates.NotSafe: - //Lets check if we are at a safe distance from the source vessel - using (var hitsEnu = Physics.OverlapSphere(futureMissilePosition, GetBlastRadius() * 3f, (int)(LayerMasks.Parts | LayerMasks.Scenery | LayerMasks.Unknown19)).AsEnumerable().GetEnumerator()) { - while (hitsEnu.MoveNext()) + //Lets check if we are at a safe distance from the source vessel + var dist = GetBlastRadius() * 1.25f; //this is from launching vessel, which assuming is also moving forward on a similar vector, could potentially result in missiles not arming for several km for faster planes/slower missiles + var hitCount = Physics.OverlapSphereNonAlloc(futureMissilePosition, dist, proximityHitColliders, layerMask); + if (hitCount == proximityHitColliders.Length) + { + proximityHitColliders = Physics.OverlapSphere(futureMissilePosition, dist, layerMask); + hitCount = proximityHitColliders.Length; + } + using (var hitsEnu = proximityHitColliders.Take(hitCount).GetEnumerator()) { - if (hitsEnu.Current == null) continue; - try + while (hitsEnu.MoveNext()) { - Part partHit = hitsEnu.Current.GetComponentInParent(); - if (partHit == null) continue; - if (ProjectileUtils.IsIgnoredPart(partHit)) continue; // Ignore ignored parts. + if (hitsEnu.Current == null) continue; + try + { + Part partHit = hitsEnu.Current.GetComponentInParent(); + if (partHit == null) continue; + if (ProjectileUtils.IsIgnoredPart(partHit)) continue; // Ignore ignored parts. - if (partHit.vessel != vessel && partHit.vessel == SourceVessel) // Not ourselves, but the source vessel. + if (partHit.vessel != vessel && partHit.vessel == SourceVessel) // Not ourselves, but the source vessel. + { + //We found a hit to the vessel + return; + } + } + catch (Exception e) { - //We found a hit to the vessel - return; + // ignored + Debug.LogWarning("[BDArmory.MissileBase]: Exception thrown in CheckDetonatationState: " + e.Message + "\n" + e.StackTrace); } } - catch (Exception e) - { - // ignored - Debug.LogWarning("[BDArmory.MissileBase]: Exception thrown in CheckDetonatationState: " + e.Message + "\n" + e.StackTrace); - } } - } - //We are safe and we can continue with the cruising phase - DetonationDistanceState = DetonationDistanceStates.Cruising; - break; + //We are safe and we can continue with the cruising phase + DetonationDistanceState = DetonationDistanceStates.Cruising; + if (!separateWarheads) SetupExplosive(this.part); //moving arming of warhead to here from launch to prevent Laser anti-missile systems zapping a missile immediately after launch and fragging the launching plane as the missile detonates + break; + } case DetonationDistanceStates.Cruising: - if (Vector3.Distance(futureMissilePosition, futureTargetPosition) < GetBlastRadius() * 10) - { - //We are now close enough to start checking the detonation distance - DetonationDistanceState = DetonationDistanceStates.CheckingProximity; - } - else { - BDModularGuidance bdModularGuidance = this as BDModularGuidance; + if (!TargetAcquired) return; + //if (Vector3.Distance(futureMissilePosition, futureTargetPosition) < GetBlastRadius() * 10) + // Replaced old proximity check with proximity check based on either detonation distance or distance traveled per frame + if ((futureMissilePosition - futureTargetPosition).sqrMagnitude < 100 * (relativeSpeed > DetonationDistance ? relativeSpeed * relativeSpeed : DetonationDistance * DetonationDistance)) + { + //We are now close enough to start checking the detonation distance + DetonationDistanceState = DetonationDistanceStates.CheckingProximity; + } + else + { + BDModularGuidance bdModularGuidance = this as BDModularGuidance; - if (bdModularGuidance == null) return; + if (bdModularGuidance == null) return; - if (Vector3.Distance(futureMissilePosition, futureTargetPosition) > this.DetonationDistance) return; + //if (Vector3.Distance(futureMissilePosition, futureTargetPosition) > this.DetonationDistance) return; + if ((futureMissilePosition - futureTargetPosition).sqrMagnitude > DetonationDistance * DetonationDistance) return; - DetonationDistanceState = DetonationDistanceStates.CheckingProximity; + DetonationDistanceState = DetonationDistanceStates.CheckingProximity; + } + break; } - break; case DetonationDistanceStates.CheckingProximity: - if (DetonationDistance == 0) { - if (weaponClass == WeaponClasses.Bomb) return; - - if (TimeIndex > 1f) + if (!TargetAcquired) return; + if (DetonationDistance == 0) { - //Vector3 floatingorigin_current = FloatingOrigin.Offset; + if (weaponClass == WeaponClasses.Bomb) return; - Ray rayFuturePosition = new Ray(vessel.CoM, futureMissilePosition); - - var hitsFuture = Physics.RaycastAll(rayFuturePosition, (float)missileDistancePerFrame.magnitude, (int)(LayerMasks.Parts | LayerMasks.Scenery | LayerMasks.Unknown19)).AsEnumerable(); - - using (var hitsEnu = hitsFuture.GetEnumerator()) + if (TimeIndex > 1f) { - while (hitsEnu.MoveNext()) + Ray rayFuturePosition = new Ray(vessel.CoM, futureMissilePosition); + var dist = (float)missileDistancePerFrame.magnitude; + var hitCount = Physics.RaycastNonAlloc(rayFuturePosition, proximityHits, dist, layerMask); + if (hitCount == proximityHits.Length) // If there's a whole bunch of stuff in the way (unlikely), then we need to increase the size of our hits buffer. { - RaycastHit hit = hitsEnu.Current; + proximityHits = Physics.RaycastAll(rayFuturePosition, dist, layerMask); + hitCount = proximityHits.Length; + } + if (hitCount > 0) + { + Array.Sort(proximityHits, 0, hitCount, RaycastHitComparer.raycastHitComparer); - try + using (var hitsEnu = proximityHits.Take(hitCount).GetEnumerator()) { - var hitPart = hit.collider.gameObject.GetComponentInParent(); - if (hitPart == null) continue; - if (ProjectileUtils.IsIgnoredPart(hitPart)) continue; // Ignore ignored parts. - - if (hitPart.vessel != SourceVessel && hitPart.vessel != vessel) + while (hitsEnu.MoveNext()) { - //We found a hit to other vessel - vessel.SetPosition(hit.point); - DetonationDistanceState = DetonationDistanceStates.Detonate; - Detonate(); - return; + RaycastHit hit = hitsEnu.Current; + + try + { + var hitPart = hit.collider.gameObject.GetComponentInParent(); + if (hitPart == null) continue; + if (ProjectileUtils.IsIgnoredPart(hitPart)) continue; // Ignore ignored parts. + + if (hitPart.vessel != SourceVessel && hitPart.vessel != vessel) + { + //We found a hit to other vessel + vessel.SetPosition(hit.point - 0.5f * missileDistancePerFrame.normalized); + DetonationDistanceState = DetonationDistanceStates.Detonate; + Detonate(); + return; + } + } + catch (Exception e) + { + // ignored + Debug.LogWarning("[BDArmory.MissileBase]: Exception thrown in CheckDetonatationState: " + e.Message + "\n" + e.StackTrace); + } } } - catch (Exception e) - { - // ignored - Debug.LogWarning("[BDArmory.MissileBase]: Exception thrown in CheckDetonatationState: " + e.Message + "\n" + e.StackTrace); - } } } } - - previousPos = part.transform.position; - } - else - { - float optimalDistance = (float)(Math.Max(DetonationDistance, relativeSpeed)); - using (var hitsEnu = Physics.OverlapSphere(vessel.CoM, optimalDistance, (int)(LayerMasks.Parts | LayerMasks.Scenery | LayerMasks.Unknown19)).AsEnumerable().GetEnumerator()) + else { - while (hitsEnu.MoveNext()) + float optimalDistance = (float)(Math.Max(DetonationDistance, relativeSpeed)); + Vector3 targetPoint = (warheadType == WarheadTypes.ContinuousRod ? vessel.CoM - VectorUtils.GetUpDirection(TargetPosition) * (GetBlastRadius() > 0 ? (DetonationDistance / 3) : 5) : vessel.CoM); + var hitCount = Physics.OverlapSphereNonAlloc(targetPoint, optimalDistance, proximityHitColliders, layerMask); + if (hitCount == proximityHitColliders.Length) { - if (hitsEnu.Current == null) continue; - - try + proximityHitColliders = Physics.OverlapSphere(targetPoint, optimalDistance, layerMask); + hitCount = proximityHitColliders.Length; + } + using (var hitsEnu = proximityHitColliders.Take(hitCount).GetEnumerator()) + { + while (hitsEnu.MoveNext()) { - Part partHit = hitsEnu.Current.GetComponentInParent(); + if (hitsEnu.Current == null) continue; - if (partHit == null) continue; - if (ProjectileUtils.IsIgnoredPart(partHit)) continue; // Ignore ignored parts. - if (partHit.vessel == vessel || partHit.vessel == SourceVessel) continue; - if (partHit.vessel.vesselType == VesselType.Debris) continue; // Ignore debris + try + { + Part partHit = hitsEnu.Current.GetComponentInParent(); + + if (partHit == null) continue; + if (ProjectileUtils.IsIgnoredPart(partHit)) continue; // Ignore ignored parts. + if (partHit.vessel == vessel || partHit.vessel == SourceVessel) continue; + if (partHit.vessel.vesselType == VesselType.Debris) continue; // Ignore debris - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.MissileBase]: Missile proximity sphere hit | Distance overlap = " + optimalDistance + "| Part name = " + partHit.name); + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.MissileBase]: Missile proximity sphere hit | Distance overlap = " + optimalDistance + "| Part name = " + partHit.name); - //We found a hit a different vessel than ours - if (DetonateAtMinimumDistance) + //We found a hit a different vessel than ours + if (DetonateAtMinimumDistance) + { + var distanceSqr = (partHit.transform.position - vessel.CoM).sqrMagnitude; + var predictedDistanceSqr = (AIUtils.PredictPosition(partHit.transform.position, partHit.vessel.Velocity(), partHit.vessel.acceleration, Time.deltaTime) - AIUtils.PredictPosition(vessel, Time.deltaTime)).sqrMagnitude; + + //float missileDistFrame = Time.fixedDeltaTime * (float)vessel.srfSpeed; vessel.Velocity() * Time.fixedDeltaTime + + if (distanceSqr > predictedDistanceSqr && distanceSqr > relativeSpeed * relativeSpeed) // If we're closing and not going to hit within the next update, then wait. + return; + } + DetonationDistanceState = DetonationDistanceStates.Detonate; + return; + } + catch (Exception e) { - var distance = Vector3.Distance(partHit.transform.position, vessel.CoM); - var predictedDistance = Vector3.Distance(AIUtils.PredictPosition(partHit.transform.position, partHit.vessel.Velocity(), partHit.vessel.acceleration, Time.deltaTime), AIUtils.PredictPosition(vessel, Time.deltaTime)); - if (distance > predictedDistance && distance > Time.fixedDeltaTime * (float)vessel.srfSpeed) // If we're closing and not going to hit within the next update, then wait. - return; + // ignored + Debug.LogWarning("[BDArmory.MissileBase]: Exception thrown in CheckDetonatationState: " + e.Message + "\n" + e.StackTrace); } - DetonationDistanceState = DetonationDistanceStates.Detonate; - return; - } - catch (Exception e) - { - // ignored - Debug.LogWarning("[BDArmory.MissileBase]: Exception thrown in CheckDetonatationState: " + e.Message + "\n" + e.StackTrace); } } } + break; } - - break; } - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_MISSILES) { - Debug.Log("[BDArmory.MissileBase]: DetonationDistanceState = : " + DetonationDistanceState); + Debug.Log($"[BDArmory.MissileBase]: DetonationDistanceState = : {DetonationDistanceState}"); } } @@ -1158,7 +1382,7 @@ protected void SetInitialDetonationDistance() { if (this.DetonationDistance == -1) { - if (GuidanceMode == GuidanceModes.AAMLead || GuidanceMode == GuidanceModes.AAMPure) + if (GuidanceMode == GuidanceModes.AAMLead || GuidanceMode == GuidanceModes.AAMPure || GuidanceMode == GuidanceModes.PN || GuidanceMode == GuidanceModes.APN || GuidanceMode == GuidanceModes.AAMLoft )//|| GuidanceMode == GuidanceModes.AAMHybrid) { DetonationDistance = GetBlastRadius() * 0.25f; } @@ -1168,9 +1392,9 @@ protected void SetInitialDetonationDistance() DetonationDistance = 0f; } } - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_MISSILES) { - Debug.Log("[BDArmory.MissileBase]: DetonationDistance = : " + DetonationDistance); + Debug.Log($"[BDArmory.MissileBase]: DetonationDistance = : {DetonationDistance}"); } } @@ -1187,7 +1411,7 @@ protected void CollisionEnter(Collision col) if (DetonationDistanceState != DetonationDistanceStates.CheckingProximity) return; - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.MissileBase]: Missile Collided - Triggering Detonation"); + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.MissileBase]: Missile Collided - Triggering Detonation"); Detonate(); } @@ -1214,4 +1438,13 @@ public void CruiseAltitudeRange() this.part.RefreshAssociatedWindows(); } } + + internal class RaycastHitComparer : IComparer + { + int IComparer.Compare(RaycastHit left, RaycastHit right) + { + return left.distance.CompareTo(right.distance); + } + public static RaycastHitComparer raycastHitComparer = new RaycastHitComparer(); + } } diff --git a/BDArmory/Weapons/Missiles/MissileDummy.cs b/BDArmory/Weapons/Missiles/MissileDummy.cs new file mode 100644 index 000000000..7904b0d00 --- /dev/null +++ b/BDArmory/Weapons/Missiles/MissileDummy.cs @@ -0,0 +1,91 @@ +using BDArmory.Utils; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; + +namespace BDArmory.Weapons.Missiles +{ + class MissileDummy : MonoBehaviour + { + /// + /// Create a Dummy missile model to attach to VLS or other multi-missile launchers prior to launch and the actual missile created and fired. + /// + Part parentPart; + static bool hasOnVesselUnloaded = false; + public static ObjectPool CreateDummyPool(string modelPath) + { + if ((Versioning.version_major == 1 && Versioning.version_minor > 10) || Versioning.version_major > 1) // onVesselUnloaded event introduced in 1.11 + hasOnVesselUnloaded = true; + GameObject template = GameDatabase.Instance.GetModel(modelPath); + template.SetActive(false); + template.AddComponent(); + + return ObjectPool.CreateObjectPool(template, 10, true, true, 0, true); + } + + public MissileDummy AttachAt(Part Parent, Transform missileTransform) + { + if (Parent is null) return null; + parentPart = Parent; + transform.SetParent(Parent.transform); + transform.position = missileTransform.position; + transform.rotation = missileTransform.rotation; + transform.parent = missileTransform; + parentPart.OnJustAboutToDie += OnParentDestroy; + parentPart.OnJustAboutToBeDestroyed += OnParentDestroy; + if (hasOnVesselUnloaded) + { + OnVesselUnloaded_1_11(false); // Remove any previous onVesselUnloaded event handler (due to forced reuse in the pool). + OnVesselUnloaded_1_11(true); // Catch unloading events too. + } + gameObject.SetActive(true); + return this; + } + + void OnParentDestroy() + { + if (parentPart is not null) + { + parentPart.OnJustAboutToDie -= OnParentDestroy; + parentPart.OnJustAboutToBeDestroyed -= OnParentDestroy; + Deactivate(); + } + } + + void OnVesselUnloaded(Vessel vessel) + { + if (parentPart is not null && (parentPart.vessel is null || parentPart.vessel == vessel)) + { + OnParentDestroy(); + } + else if (parentPart is null) + { + Deactivate(); + } + } + void OnVesselUnloaded_1_11(bool addRemove) // onVesselUnloaded event introduced in 1.11 + { + if (addRemove) + GameEvents.onVesselUnloaded.Add(OnVesselUnloaded); + else + GameEvents.onVesselUnloaded.Remove(OnVesselUnloaded); + } + public void Deactivate() + { + if (gameObject is not null && gameObject.activeSelf) // Deactivate even if a parent is already inactive. + { + parentPart = null; + transform.parent = null; + gameObject.SetActive(false); + } + } + public void OnDestroy() // This shouldn't be happening except on exiting KSP, but sometimes they get destroyed instead of disabled! + { + if (hasOnVesselUnloaded) // onVesselUnloaded event introduced in 1.11 + OnVesselUnloaded_1_11(false); + } + } +} diff --git a/BDArmory/Weapons/Missiles/MissileLaunchParams.cs b/BDArmory/Weapons/Missiles/MissileLaunchParams.cs new file mode 100644 index 000000000..0feffe019 --- /dev/null +++ b/BDArmory/Weapons/Missiles/MissileLaunchParams.cs @@ -0,0 +1,115 @@ +using UnityEngine; + +using BDArmory.Control; +using BDArmory.Extensions; +using BDArmory.Settings; +using BDArmory.Utils; + +namespace BDArmory.Weapons.Missiles +{ + public struct MissileLaunchParams + { + public float minLaunchRange; + public float maxLaunchRange; + + private float rtr; + + /// + /// Gets the maximum no-escape range. + /// + /// The max no-escape range. + public float rangeTr + { + get + { + return rtr; + } + } + + public MissileLaunchParams(float min, float max) + { + minLaunchRange = min; + maxLaunchRange = max; + rtr = (max + min) / 2; + } + + /// + /// Gets the dynamic launch parameters. + /// + /// The dynamic launch parameters. + /// Launcher velocity. + /// Target velocity. + /// Target position. + /// If non-negative, restrict the calculations to assuming the launcher velocity is at most this angle off-target. Avoids extreme extending ranges. + public static MissileLaunchParams GetDynamicLaunchParams(MissileBase missile, Vector3 targetVelocity, Vector3 targetPosition, float maxAngleOffTarget = -1, bool unguidedGuidedMissile = false) + { + if (missile == null || missile.part == null) return new MissileLaunchParams(0, 0); // Safety check in case the missile part is being destroyed at the same time. + Vector3 launcherVelocity = missile.vessel.Velocity(); + Vector3 launcherPosition = missile.part.transform.position; + Vector3 vectorToTarget = targetPosition - launcherPosition; + if (maxAngleOffTarget >= 0) { launcherVelocity = Vector3.RotateTowards(vectorToTarget, launcherVelocity, maxAngleOffTarget, 0); } + + bool surfaceLaunch = missile.vessel.LandedOrSplashed; + float launcherSpeed = (float)missile.vessel.srfSpeed; + float minLaunchRange = missile.minStaticLaunchRange; + float maxLaunchRange = missile.maxStaticLaunchRange; + if (unguidedGuidedMissile) maxLaunchRange /= 10; + float bodyGravity = (float)PhysicsGlobals.GravitationalAcceleration * (float)missile.vessel.orbit.referenceBody.GeeASL; // Set gravity for calculations; + float missileActiveTime = 2f; + float rangeAddMax = 0; + float relSpeed; + + // Calculate relative speed + Vector3 relV = targetVelocity - launcherVelocity; + Vector3 relVProjected = Vector3.Project(relV, vectorToTarget); + relSpeed = -Mathf.Sign(Vector3.Dot(relVProjected, vectorToTarget)) * relVProjected.magnitude; // Positive value when targets are closing on each other, negative when they are flying apart + + if (missile.GetComponent() == null) + { + // Basic time estimate for missile to drop and travel a safe distance from vessel assuming constant acceleration and firing vessel not accelerating + MissileLauncher ml = missile.GetComponent(); + float maxMissileAccel = ml.thrust / missile.part.mass; + float blastRadius = Mathf.Min(missile.GetBlastRadius(), 150f); // Allow missiles with absurd blast ranges to still be launched if desired + missileActiveTime = Mathf.Min((surfaceLaunch ? 0f : missile.dropTime) + BDAMath.Sqrt(2 * blastRadius / maxMissileAccel), 2f); // Clamp at 2s for now + Vector3 missileFwd = missile.GetForwardTransform(); + if (maxAngleOffTarget >= 0) { missileFwd = Vector3.RotateTowards(vectorToTarget, missileFwd, maxAngleOffTarget, 0); } + + if ((Vector3.Dot(vectorToTarget, missileFwd) < 0.965f) || ((!surfaceLaunch) && (missile.GetWeaponClass() != WeaponClasses.SLW) && (ml.guidanceActive))) // Only evaluate missile turning ability if the target is outside ~15 deg cone, or isn't a torpedo and has guidance + { + // Rough range estimate of max missile G in a turn after launch, the following code is quite janky but works decently well in practice + float maxEstimatedGForce = Mathf.Max(bodyGravity * ml.maxTorque, 15f); // Rough estimate of max G based on missile torque, use minimum of 15G to prevent some VLS parts from not working + if (ml.aero) // If missile has aerodynamics, modify G force by AoA limit + { + maxEstimatedGForce *= Mathf.Sin(ml.maxAoA * Mathf.Deg2Rad); + } + + // Rough estimate of turning radius and arc length to travel + float arcLength = 0; + float futureTime = Mathf.Clamp((surfaceLaunch ? 0f : missile.dropTime), 0f, 2f); + Vector3 futureRelPosition = (targetPosition + targetVelocity * futureTime) - (launcherPosition + launcherVelocity * futureTime); + float missileTurnRadius = (ml.optimumAirspeed * ml.optimumAirspeed) / maxEstimatedGForce; + float targetAngle = Vector3.Angle(missileFwd, futureRelPosition); + arcLength = Mathf.Deg2Rad * targetAngle * missileTurnRadius; + + // Add additional range term for the missile to manuever to target at missileActiveTime + minLaunchRange = Mathf.Max(arcLength, minLaunchRange); + } + } + + float missileMaxRangeTime = 8f; // Placeholder value since this doesn't really matter much in BDA combat + + // Adjust ranges + minLaunchRange = Mathf.Min(minLaunchRange + relSpeed * missileActiveTime, minLaunchRange); + rangeAddMax += relSpeed * missileMaxRangeTime; + + // Add altitude term to max + double diffAlt = missile.vessel.altitude - FlightGlobals.getAltitudeAtPos(targetPosition); + rangeAddMax += (float)diffAlt; + + float min = Mathf.Clamp(minLaunchRange, 0, BDArmorySettings.MAX_ENGAGEMENT_RANGE); + float max = Mathf.Clamp(maxLaunchRange + rangeAddMax, min + 100, BDArmorySettings.MAX_ENGAGEMENT_RANGE); + + return new MissileLaunchParams(min, max); + } + } +} diff --git a/BDArmory/Modules/MissileLauncher.cs b/BDArmory/Weapons/Missiles/MissileLauncher.cs similarity index 56% rename from BDArmory/Modules/MissileLauncher.cs rename to BDArmory/Weapons/Missiles/MissileLauncher.cs index 2a8478111..b0e36d345 100644 --- a/BDArmory/Modules/MissileLauncher.cs +++ b/BDArmory/Weapons/Missiles/MissileLauncher.cs @@ -2,28 +2,34 @@ using System.Collections; using System.Collections.Generic; using System.Text; -using BDArmory.Core; -using BDArmory.Core.Extension; -using BDArmory.Core.Utils; +using UniLinq; +using UnityEngine; + +using BDArmory.Control; +using BDArmory.Extensions; using BDArmory.FX; using BDArmory.Guidances; -using BDArmory.Misc; -using BDArmory.Parts; using BDArmory.Radar; +using BDArmory.Settings; using BDArmory.Targeting; using BDArmory.UI; -using UniLinq; -using UnityEngine; +using BDArmory.Utils; +using BDArmory.WeaponMounts; -namespace BDArmory.Modules +namespace BDArmory.Weapons.Missiles { - public class MissileLauncher : MissileBase + public class MissileLauncher : MissileBase, IPartMassModifier { + public Coroutine reloadRoutine; + Coroutine reloadableMissile; #region Variable Declarations [KSPField] public string homingType = "AAM"; + [KSPField] + public float pronavGain = 3f; + [KSPField] public string targetingType = "none"; @@ -34,6 +40,11 @@ public class MissileLauncher : MissileBase public MissileTurret missileTurret = null; public BDRotaryRail rotaryRail = null; public BDDeployableRail deployableRail = null; + public MultiMissileLauncher multiLauncher = null; + private BDStagingAreaGauge gauge; + private float reloadTimer = 0; + public float heatTimer = -1; + private Vector3 origScale = Vector3.one; [KSPField] public string exhaustPrefabPath; @@ -109,7 +120,7 @@ public class MissileLauncher : MissileBase public float optimumAirspeed = 220; [KSPField] - public float blastRadius = 150; + public float blastRadius = -1; [KSPField] public float blastPower = 25; @@ -192,7 +203,7 @@ public class MissileLauncher : MissileBase List pEmitters; List gaplessEmitters; - float cmTimer; + //float cmTimer; //deploy animation [KSPField] @@ -240,6 +251,8 @@ public class MissileLauncher : MissileBase List boosters; + List fairings; + [KSPField] public bool decoupleBoosters = false; @@ -249,6 +262,16 @@ public class MissileLauncher : MissileBase [KSPField] public float boosterMass = 0; + //Fuel Weight variables + [KSPField] + public float boosterFuelMass = 0; + + [KSPField] + public float cruiseFuelMass = 0; + + [KSPField] + public bool useFuel = false; + Transform vesselReferenceTransform; [KSPField] @@ -256,6 +279,9 @@ public class MissileLauncher : MissileBase List boostEmitters; List boostGaplessEmitters; + [KSPField] + public string fairingTransformName = string.Empty; + [KSPField] public bool torpedo = false; @@ -264,16 +290,36 @@ public class MissileLauncher : MissileBase //ballistic options [KSPField] - public bool indirect = false; + public bool indirect = false; //unused [KSPField] public bool vacuumSteerable = true; + // Loft Options + [KSPField] + public string terminalHomingType = "pronav"; + + [KSPField] + public float LoftTermRange = -1; + public GPSTargetInfo designatedGPSInfo; float[] rcsFiredTimes; KSPParticleEmitter[] rcsTransforms; + private bool OldInfAmmo = false; + private bool StartSetupComplete = false; + + //Fuel Burn Variables + public float GetModuleMass(float baseMass, ModifierStagingSituation situation) => -burnedFuelMass; + public ModifierChangeWhen GetModuleMassChangeWhen() => ModifierChangeWhen.CONSTANTLY; + + private float burnRate = 0; + private float burnedFuelMass = 0; + + public bool SetupComplete => StartSetupComplete; + public int loftState = 0; + public float initMaxAoA = 0; #endregion Variable Declarations [KSPAction("Fire Missile")] @@ -326,7 +372,7 @@ public void GuiFire() public override void Jettison() { if (missileTurret) return; - + if (multiLauncher && !multiLauncher.permitJettison) return; part.decouple(0); if (BDArmorySetup.Instance.ActiveWeaponManager != null) BDArmorySetup.Instance.ActiveWeaponManager.UpdateList(); } @@ -353,17 +399,51 @@ void ParseWeaponClass() weaponClass = WeaponClasses.Missile; } } - public override void OnStart(StartState state) { //base.OnStart(state); - ParseWeaponClass(); + + if (useFuel) + { + float initialMass = part.mass; + if (boosterFuelMass < 0 || boostTime <= 0) + { + if (boosterFuelMass < 0) Debug.LogWarning($"[BDArmory.MissileLauncher]: Error in configuration of {part.name}, boosterFuelMass: {boosterFuelMass} can't be less than 0, reverting to default value."); + boosterFuelMass = 0; + } + + if (cruiseFuelMass < 0 || cruiseTime <= 0) + { + if (cruiseFuelMass < 0) Debug.LogWarning($"[BDArmory.MissileLauncher]: Error in configuration of {part.name}, cruiseFuelMass: {cruiseFuelMass} can't be less than 0, reverting to default value."); + cruiseFuelMass = 0; + } + + if (boosterFuelMass + cruiseFuelMass > initialMass * 0.95f) + { + Debug.LogWarning($"[BDArmory.MissileLauncher]: Error in configuration of {part.name}, boosterFuelMass: {boosterFuelMass} + cruiseFuelMass: {cruiseFuelMass} can't be greater than 95% of the missile mass {initialMass}, clamping to 80% of the missile mass."); + if(boosterFuelMass > 0 || boostTime > 0) + { + if(cruiseFuelMass > 0 || cruiseTime > 0) + { + boosterFuelMass = Mathf.Clamp(boosterFuelMass, 0, initialMass * 0.4f); + cruiseFuelMass = Mathf.Clamp(cruiseFuelMass, 0, initialMass * 0.4f); + } + else boosterFuelMass = Mathf.Clamp(boosterFuelMass, 0, initialMass * 0.8f); + } + else cruiseFuelMass = Mathf.Clamp(cruiseFuelMass, 0, initialMass * 0.8f); + } + else + { + if (boostTime > 0 && boosterFuelMass <= 0) boosterFuelMass = initialMass * 0.1f; + if (cruiseTime > 0 && cruiseFuelMass <= 0) cruiseFuelMass = initialMass * 0.1f; + } + } if (shortName == string.Empty) { shortName = part.partInfo.title; } - + if (BDArmorySettings.DEBUG_MISSILES) shortName = $"{SourceVessel.GetName()}'s {GetShortName()}"; gaplessEmitters = new List(); pEmitters = new List(); boostEmitters = new List(); @@ -376,21 +456,20 @@ public override void OnStart(StartState state) Fields["minStaticLaunchRange"].guiActive = false; Fields["minStaticLaunchRange"].guiActiveEditor = false; - if (isTimed) + loftState = 0; + TimeToImpact = float.PositiveInfinity; + initMaxAoA = maxAoA; + terminalHomingActive = false; + + if (LoftTermRange > 0) { - Fields["detonationTime"].guiActive = true; - Fields["detonationTime"].guiActiveEditor = true; - } - else - { - Fields["detonationTime"].guiActive = false; - Fields["detonationTime"].guiActiveEditor = false; + Debug.LogWarning($"[BDArmory.MissileLauncher]: Error in configuration of {part.name}, LoftTermRange is deprecated, please use terminalHomingRange instead."); + terminalHomingRange = LoftTermRange; + LoftTermRange = -1; } - ParseModes(); ParseAntiRadTargetTypes(); // extension for feature_engagementenvelope - InitializeEngagementRange(minStaticLaunchRange, maxStaticLaunchRange); using (var pEemitter = part.FindModelComponents().GetEnumerator()) while (pEemitter.MoveNext()) @@ -402,15 +481,20 @@ public override void OnStart(StartState state) if (HighLogic.LoadedSceneIsFlight) { - //TODO: Backward compatibility wordaround - if (part.FindModuleImplementing() == null) - { - FromBlastPowerToTNTMass(); - } - else + missileName = part.name; + if (warheadType == WarheadTypes.Standard || warheadType == WarheadTypes.ContinuousRod) { + var tnt = part.FindModuleImplementing(); + if (tnt is null) + { + tnt = (BDExplosivePart)part.AddModule("BDExplosivePart"); + tnt.tntMass = BlastPhysicsUtils.CalculateExplosiveMass(blastRadius); + } + //New Explosive module DisablingExplosives(part); + if (tnt.explModelPath == ModuleWeapon.defaultExplModelPath) tnt.explModelPath = explModelPath; // If the BDExplosivePart is using the default explosion part and sound, + if (tnt.explSoundPath == ModuleWeapon.defaultExplSoundPath) tnt.explSoundPath = explSoundPath; // override them with those of the MissileLauncher (if specified). } MissileReferenceTransform = part.FindModelTransform("missileTransform"); @@ -419,6 +503,10 @@ public override void OnStart(StartState state) MissileReferenceTransform = part.partTransform; } + origScale = part.partTransform.localScale; + gauge = (BDStagingAreaGauge)part.AddModule("BDStagingAreaGauge"); + part.force_activate(); + if (!string.IsNullOrEmpty(exhaustPrefabPath)) { using (var t = part.FindModelTransforms("exhaustTransform").AsEnumerable().GetEnumerator()) @@ -470,6 +558,17 @@ public override void OnStart(StartState state) } } + fairings = new List(); + if (!string.IsNullOrEmpty(fairingTransformName)) + { + using (var t = part.FindModelTransforms(fairingTransformName).AsEnumerable().GetEnumerator()) + while (t.MoveNext()) + { + if (t.Current == null) continue; + fairings.Add(t.Current.gameObject); + } + } + using (var pEmitter = part.partTransform.Find("model").GetComponentsInChildren().AsEnumerable().GetEnumerator()) while (pEmitter.MoveNext()) { @@ -499,9 +598,14 @@ public override void OnStart(StartState state) } } - cmTimer = Time.time; + using (IEnumerator light = gameObject.GetComponentsInChildren().AsEnumerable().GetEnumerator()) + while (light.MoveNext()) + { + if (light.Current == null) continue; + light.Current.intensity = 0; + } - part.force_activate(); + //cmTimer = Time.time; using (var pe = pEmitters.GetEnumerator()) while (pe.MoveNext()) @@ -533,8 +637,82 @@ public override void OnStart(StartState state) KillRCS(); } SetupAudio(); + + } + + SetFields(); + + if (deployAnimationName != "") + { + deployStates = GUIUtils.SetUpAnimation(deployAnimationName, part); + } + else + { + deployedDrag = simpleDrag; + } + if (flightAnimationName != "") + { + animStates = GUIUtils.SetUpAnimation(flightAnimationName, part); + } + + IEnumerator partModules = part.Modules.GetEnumerator(); + while (partModules.MoveNext()) + { + if (partModules.Current == null) continue; + if (partModules.Current.moduleName == "BDExplosivePart") + { + ((BDExplosivePart)partModules.Current).ParseWarheadType(); + if (((BDExplosivePart)partModules.Current).warheadReportingName == "Continuous Rod") + { + warheadType = WarheadTypes.ContinuousRod; + } + else warheadType = WarheadTypes.Standard; + } + if (partModules.Current.moduleName == "ClusterBomb") + { + clusterbomb = ((ClusterBomb)partModules.Current).submunitions.Count; + } + if (partModules.Current.moduleName == "MultiMissileLauncher" && weaponClass == WeaponClasses.Bomb) + { + clusterbomb *= ((MultiMissileLauncher)partModules.Current).salvoSize; + } + if (partModules.Current.moduleName == "ModuleEMP") + { + warheadType = WarheadTypes.EMP; + StandOffDistance = ((ModuleEMP)partModules.Current).proximity; + } + if (partModules.Current.moduleName == "BDModuleNuke") + { + warheadType = WarheadTypes.Nuke; + StandOffDistance = BDAMath.Sqrt(((BDModuleNuke)partModules.Current).yield) * 500; + } + else continue; + break; } + partModules.Dispose(); + StartSetupComplete = true; + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.MissileLauncher] Start() setup complete"); + } + public void SetFields() + { + ParseWeaponClass(); + ParseModes(); + InitializeEngagementRange(minStaticLaunchRange, maxStaticLaunchRange); + SetInitialDetonationDistance(); + uncagedLock = (allAspect) ? allAspect : uncagedLock; + guidanceFailureRatePerFrame = (guidanceFailureRate >= 1) ? 1f : 1f - Mathf.Exp(Mathf.Log(1f - guidanceFailureRate) * Time.fixedDeltaTime); // Convert from per-second failure rate to per-frame failure rate + + if (isTimed) + { + Fields["detonationTime"].guiActive = true; + Fields["detonationTime"].guiActiveEditor = true; + } + else + { + Fields["detonationTime"].guiActive = false; + Fields["detonationTime"].guiActiveEditor = false; + } if (GuidanceMode != GuidanceModes.Cruise) { CruiseAltitudeRange(); @@ -546,12 +724,28 @@ public override void OnStart(StartState state) Events["CruiseAltitudeRange"].guiActiveEditor = false; Fields["CruisePredictionTime"].guiActiveEditor = false; } + else + { + CruiseAltitudeRange(); + Fields["CruiseAltitude"].guiActive = true; + Fields["CruiseAltitude"].guiActiveEditor = true; + Fields["CruiseSpeed"].guiActive = true; + Fields["CruiseSpeed"].guiActiveEditor = true; + Events["CruiseAltitudeRange"].guiActive = true; + Events["CruiseAltitudeRange"].guiActiveEditor = true; + Fields["CruisePredictionTime"].guiActiveEditor = true; + } if (GuidanceMode != GuidanceModes.AGM) { Fields["maxAltitude"].guiActive = false; Fields["maxAltitude"].guiActiveEditor = false; } + else + { + Fields["maxAltitude"].guiActive = true; + Fields["maxAltitude"].guiActiveEditor = true; + } if (GuidanceMode != GuidanceModes.AGMBallistic) { Fields["BallisticOverShootFactor"].guiActive = false; @@ -559,11 +753,24 @@ public override void OnStart(StartState state) Fields["BallisticAngle"].guiActive = false; Fields["BallisticAngle"].guiActiveEditor = false; } + else + { + Fields["BallisticOverShootFactor"].guiActive = true; + Fields["BallisticOverShootFactor"].guiActiveEditor = true; + Fields["BallisticAngle"].guiActive = true; + Fields["BallisticAngle"].guiActiveEditor = true; + } - if (part.partInfo.title.Contains("Bomb")) + if (part.partInfo.title.Contains("Bomb") || weaponClass == WeaponClasses.SLW) { Fields["dropTime"].guiActive = false; Fields["dropTime"].guiActiveEditor = false; + if (torpedo) dropTime = 999; + } + else + { + Fields["dropTime"].guiActive = true; + Fields["dropTime"].guiActiveEditor = true; } if (TargetingModeTerminal != TargetingModes.None) @@ -576,21 +783,85 @@ public override void OnStart(StartState state) Fields["terminalGuidanceShouldActivate"].guiActiveEditor = false; } - if (deployAnimationName != "") - { - deployStates = Utils.SetUpAnimation(deployAnimationName, part); + if (GuidanceMode != GuidanceModes.AAMLoft) + { + Fields["LoftMaxAltitude"].guiActive = false; + Fields["LoftMaxAltitude"].guiActiveEditor = false; + Fields["LoftRangeOverride"].guiActive = false; + Fields["LoftRangeOverride"].guiActiveEditor = false; + Fields["LoftAltitudeAdvMax"].guiActive = false; + Fields["LoftAltitudeAdvMax"].guiActiveEditor = false; + Fields["LoftMinAltitude"].guiActive = false; + Fields["LoftMinAltitude"].guiActiveEditor = false; + Fields["LoftAngle"].guiActive = false; + Fields["LoftAngle"].guiActiveEditor = false; + Fields["LoftTermAngle"].guiActive = false; + Fields["LoftTermAngle"].guiActiveEditor = false; + Fields["LoftRangeFac"].guiActive = false; + Fields["LoftRangeFac"].guiActiveEditor = false; + Fields["LoftVelComp"].guiActive = false; + Fields["LoftVelComp"].guiActiveEditor = false; + Fields["LoftVertVelComp"].guiActive = false; + Fields["LoftVertVelComp"].guiActiveEditor = false; + //Fields["LoftAltComp"].guiActive = false; + //Fields["LoftAltComp"].guiActiveEditor = false; + //Fields["terminalHomingRange"].guiActive = false; + //Fields["terminalHomingRange"].guiActiveEditor = false; } else { - deployedDrag = simpleDrag; + Fields["LoftMaxAltitude"].guiActive = true; + Fields["LoftMaxAltitude"].guiActiveEditor = true; + Fields["LoftRangeOverride"].guiActive = true; + Fields["LoftRangeOverride"].guiActiveEditor = true; + Fields["LoftAltitudeAdvMax"].guiActive = true; + Fields["LoftAltitudeAdvMax"].guiActiveEditor = true; + Fields["LoftMinAltitude"].guiActive = true; + Fields["LoftMinAltitude"].guiActiveEditor = true; + //Fields["terminalHomingRange"].guiActive = true; + //Fields["terminalHomingRange"].guiActiveEditor = true; + + if (!GameSettings.ADVANCED_TWEAKABLES) + { + Fields["LoftAngle"].guiActive = false; + Fields["LoftAngle"].guiActiveEditor = false; + Fields["LoftTermAngle"].guiActive = false; + Fields["LoftTermAngle"].guiActiveEditor = false; + Fields["LoftRangeFac"].guiActive = false; + Fields["LoftRangeFac"].guiActiveEditor = false; + Fields["LoftVelComp"].guiActive = false; + Fields["LoftVelComp"].guiActiveEditor = false; + Fields["LoftVertVelComp"].guiActive = false; + Fields["LoftVertVelComp"].guiActiveEditor = false; + //Fields["LoftAltComp"].guiActive = false; + //Fields["LoftAltComp"].guiActiveEditor = false; + } + else + { + Fields["LoftAngle"].guiActive = true; + Fields["LoftAngle"].guiActiveEditor = true; + Fields["LoftTermAngle"].guiActive = true; + Fields["LoftTermAngle"].guiActiveEditor = true; + Fields["LoftRangeFac"].guiActive = true; + Fields["LoftRangeFac"].guiActiveEditor = true; + Fields["LoftVelComp"].guiActive = true; + Fields["LoftVelComp"].guiActiveEditor = true; + Fields["LoftVertVelComp"].guiActive = true; + Fields["LoftVertVelComp"].guiActiveEditor = true; + //Fields["LoftAltComp"].guiActive = true; + //Fields["LoftAltComp"].guiActiveEditor = true; + } } - - if (flightAnimationName != "") + if (!terminalHoming && GuidanceMode != GuidanceModes.AAMLoft) //(GuidanceMode != GuidanceModes.AAMHybrid && GuidanceMode != GuidanceModes.AAMLoft) { - animStates = Utils.SetUpAnimation(flightAnimationName, part); + Fields["terminalHomingRange"].guiActive = false; + Fields["terminalHomingRange"].guiActiveEditor = false; + } + else + { + Fields["terminalHomingRange"].guiActive = true; + Fields["terminalHomingRange"].guiActiveEditor = true; } - - SetInitialDetonationDistance(); // fill lockedSensorFOVBias with default values if not set by part config: if ((TargetingMode == TargetingModes.Heat || TargetingModeTerminal == TargetingModes.Heat) && heatThreshold > 0 && lockedSensorFOVBias.minTime == float.MaxValue) @@ -598,11 +869,11 @@ public override void OnStart(StartState state) float a = lockedSensorFOV / 2f; float b = -1f * ((1f - 1f / 1.2f)); float[] x = new float[6] { 0f * a, 0.2f * a, 0.4f * a, 0.6f * a, 0.8f * a, 1f * a }; - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.MissileLauncher]: OnStart missile " + shortName + ": setting default lockedSensorFOVBias curve to:"); + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileLauncher]: OnStart missile {shortName}: setting default lockedSensorFOVBias curve to:"); for (int i = 0; i < 6; i++) { lockedSensorFOVBias.Add(x[i], b / (a * a) * x[i] * x[i] + 1f, -1f / 3f * x[i] / (a * a), -1f / 3f * x[i] / (a * a)); - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("key = " + x[i] + " " + (b / (a * a) * x[i] * x[i] + 1f) + " " + (-1f / 3f * x[i] / (a * a)) + " " + (-1f / 3f * x[i] / (a * a))); + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("key = " + x[i] + " " + (b / (a * a) * x[i] * x[i] + 1f) + " " + (-1f / 3f * x[i] / (a * a)) + " " + (-1f / 3f * x[i] / (a * a))); } } @@ -611,9 +882,9 @@ public override void OnStart(StartState state) { lockedSensorVelocityBias.Add(0f, 1f); lockedSensorVelocityBias.Add(180f, 1f); - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_MISSILES) { - Debug.Log("[BDArmory.MissileLauncher]: OnStart missile " + shortName + ": setting default lockedSensorVelocityBias curve to:"); + Debug.Log($"[BDArmory.MissileLauncher]: OnStart missile {shortName}: setting default lockedSensorVelocityBias curve to:"); Debug.Log("key = 0 1"); Debug.Log("key = 180 1"); } @@ -624,24 +895,9 @@ public override void OnStart(StartState state) { activeRadarLockTrackCurve.Add(0f, 0f); activeRadarLockTrackCurve.Add(activeRadarRange, RadarUtils.MISSILE_DEFAULT_LOCKABLE_RCS); // TODO: tune & balance constants! - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.MissileLauncher]: OnStart missile " + shortName + ": setting default locktrackcurve with maxrange/minrcs: " + activeRadarLockTrackCurve.maxTime + "/" + RadarUtils.MISSILE_DEFAULT_LOCKABLE_RCS); - } - List.Enumerator cluster = part.FindModulesImplementing().GetEnumerator(); - while (cluster.MoveNext()) - { - if (cluster.Current == null) continue; - clusterbomb = cluster.Current.submunitions.Count; - break; + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileLauncher]: OnStart missile {shortName}: setting default locktrackcurve with maxrange/minrcs: {activeRadarLockTrackCurve.maxTime}/{RadarUtils.MISSILE_DEFAULT_LOCKABLE_RCS}"); } - cluster.Dispose(); - List.Enumerator emp = part.FindModulesImplementing().GetEnumerator(); - while (emp.MoveNext()) - { - if (emp.Current == null) continue; - EMP = emp.Current; - break; - } - emp.Dispose(); + GUIUtils.RefreshAssociatedWindows(part); } /// @@ -672,7 +928,7 @@ void SetupAudio() if (audioClipPath != string.Empty) { - audioSource.clip = GameDatabase.Instance.GetAudioClip(audioClipPath); + audioSource.clip = SoundUtils.GetAudioClip(audioClipPath); } if (sfAudioSource == null) @@ -687,12 +943,12 @@ void SetupAudio() if (audioClipPath != string.Empty) { - thrustAudio = GameDatabase.Instance.GetAudioClip(audioClipPath); + thrustAudio = SoundUtils.GetAudioClip(audioClipPath); } if (boostClipPath != string.Empty) { - boostAudio = GameDatabase.Instance.GetAudioClip(boostClipPath); + boostAudio = SoundUtils.GetAudioClip(boostClipPath); } UpdateVolume(); @@ -712,21 +968,6 @@ void UpdateVolume() } } - void Update() - { - if (!HasFired) - CheckDetonationState(); - if (HighLogic.LoadedSceneIsFlight) - { - if (weaponClass == WeaponClasses.SLW && FlightGlobals.getAltitudeAtPos(part.transform.position) > 0) //#710 - { - float a = (float)FlightGlobals.getGeeForceAtPosition(part.transform.position).magnitude; - float d = FlightGlobals.getAltitudeAtPos(part.transform.position); - dropTime = ((float)Math.Sqrt(a * (a + (8 * d))) - a) / (2 * a) - (Time.fixedDeltaTime * 1.5f); //quadratic equation for accel to find time from known force and vel - }// adjusts droptime to delay the MissileRoutine IEnum so torps won't start boosting until splashdown - } - } - void OnDestroy() { DetachExhaustPrefabs(); @@ -738,6 +979,9 @@ void OnDestroy() if (pEmitters != null) foreach (var pe in pEmitters) if (pe) EffectBehaviour.RemoveParticleEmitter(pe); + if (gaplessEmitters is not null) // Make sure the gapless emitters get destroyed (they should anyway, but KSP holds onto part references, which may prevent this from happening automatically). + foreach (var gpe in gaplessEmitters) + if (gpe is not null) Destroy(gpe); if (boostEmitters != null) foreach (var pe in boostEmitters) if (pe) EffectBehaviour.RemoveParticleEmitter(pe); @@ -751,52 +995,249 @@ void OnDestroy() public override float GetBlastRadius() { - if (part.FindModuleImplementing() != null) - { - return part.FindModuleImplementing().GetBlastRadius(); - } + if (blastRadius > 0) { return blastRadius; } else { - return blastRadius; + if (warheadType == WarheadTypes.EMP) + { + if (part.FindModuleImplementing() != null) + { + blastRadius = part.FindModuleImplementing().proximity; + return blastRadius; + } + else + { + blastRadius = 150; + return 150; + } + } + else if (warheadType == WarheadTypes.Nuke) + { + if (part.FindModuleImplementing() != null) + { + blastRadius = BDAMath.Sqrt(part.FindModuleImplementing().yield) * 500; + return blastRadius; + } + else + { + blastRadius = 150; + return 150; + } + } + else + { + if (part.FindModuleImplementing() != null) + { + blastRadius = part.FindModuleImplementing().GetBlastRadius(); + return blastRadius; + } + else + { + blastRadius = 150; + return blastRadius; + } + } } } public override void FireMissile() { - if (HasFired) return; + if (HasFired || launched) return; + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileLauncher]: Missile launch initiated! {vessel.vesselName}"); - try // FIXME Remove this once the fix is sufficiently tested. + var wpm = VesselModuleRegistry.GetMissileFire(SourceVessel != null ? SourceVessel : vessel, true); + if (wpm != null) Team = wpm.Team; + if (SourceVessel == null) SourceVessel = vessel; + + if (multiLauncher && multiLauncher.isMultiLauncher) + { + //multiLauncher.rippleRPM = wpm.rippleRPM; + //if (wpm.rippleRPM > 0) multiLauncher.rippleRPM = wpm.rippleRPM; + multiLauncher.Team = Team; + if (reloadableRail && reloadableRail.ammoCount >= 1 || BDArmorySettings.INFINITE_ORDINANCE) multiLauncher.fireMissile(); + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileLauncher]: firing Multilauncher! {vessel.vesselName}; {multiLauncher.subMunitionName}"); + } + else { - SetupExplosive(this.part); - HasFired = true; + if (reloadableRail && (multiLauncher && !multiLauncher.isClusterMissile) || ((multiLauncher && multiLauncher.isClusterMissile) && reloadableRail.maxAmmo > 1)) + { + if (reloadableMissile == null) reloadableMissile = StartCoroutine(FireReloadableMissile()); + launched = true; + } + else + { + TimeFired = Time.time; + part.decouple(0); + part.Unpack(); + vessel.vesselName = GetShortName(); + TargetPosition = (multiLauncher ? vessel.ReferenceTransform.position + vessel.ReferenceTransform.up * 5000 : transform.position + transform.forward * 5000); //set initial target position so if no target update, missileBase will count a miss if it nears this point or is flying post-thrust + MissileLaunch(); + BDATargetManager.FiredMissiles.Add(this); + if (wpm != null) wpm.heatTarget = TargetSignatureData.noTarget; + launched = true; + } + } + } + IEnumerator FireReloadableMissile() + { + part.partTransform.localScale = Vector3.zero; + part.ShieldedFromAirstream = true; + part.crashTolerance = 100; + reloadableRail.SpawnMissile(MissileReferenceTransform); + MissileLauncher ml = reloadableRail.SpawnedMissile.FindModuleImplementing(); + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileLauncher]: Spawning missile {reloadableRail.SpawnedMissile.name}; type: {ml.homingType}/{ml.targetingType}"); + yield return new WaitUntilFixed(() => ml == null || ml.SetupComplete); // Wait until missile fully initialized. + if (ml == null) + { + Debug.LogWarning($"[BDArmory.MissileLauncher]: Error while spawning missile with {part.name}, MissileLauncher was null!"); + yield break; + } - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.MissileLauncher]: Missile Fired! " + vessel.vesselName); + ml.launched = true; + GetMissileCount(); + var wpm = VesselModuleRegistry.GetMissileFire(SourceVessel, true); + BDATargetManager.FiredMissiles.Add(ml); + ml.SourceVessel = SourceVessel; + ml.GuidanceMode = GuidanceMode; + //wpm.SendTargetDataToMissile(ml); + ml.TimeFired = Time.time; + ml.vessel.vesselName = GetShortName(); + ml.DetonationDistance = DetonationDistance; + ml.DetonateAtMinimumDistance = DetonateAtMinimumDistance; + ml.dropTime = dropTime; + ml.detonationTime = detonationTime; + ml.engageAir = engageAir; + ml.engageGround = engageGround; + ml.engageMissile = engageMissile; + ml.engageSLW = engageSLW; + if (GuidanceMode == GuidanceModes.AGMBallistic) + { + ml.BallisticOverShootFactor = BallisticOverShootFactor; //are some of these null, and causeing this to quit? + ml.BallisticAngle = BallisticAngle; + } + if (GuidanceMode == GuidanceModes.Cruise) + { + ml.CruiseAltitude = CruiseAltitude; + ml.CruiseSpeed = CruiseSpeed; + ml.CruisePredictionTime = CruisePredictionTime; + } + if (GuidanceMode == GuidanceModes.AAMLoft) + { + ml.LoftMaxAltitude = LoftMaxAltitude; + ml.LoftRangeOverride = LoftRangeOverride; + ml.LoftAltitudeAdvMax = LoftAltitudeAdvMax; + ml.LoftMinAltitude = LoftMinAltitude; + ml.LoftAngle = LoftAngle; + ml.LoftTermAngle = LoftTermAngle; + ml.LoftRangeFac = LoftRangeFac; + ml.LoftVelComp = LoftVelComp; + ml.LoftVertVelComp = LoftVertVelComp; + //ml.LoftAltComp = LoftAltComp; + ml.terminalHomingRange = terminalHomingRange; + ml.homingModeTerminal = homingModeTerminal; + ml.pronavGain = pronavGain; + ml.loftState = 0; + ml.TimeToImpact = float.PositiveInfinity; + ml.initMaxAoA = maxAoA; + } +/* if (GuidanceMode == GuidanceModes.AAMHybrid) + ml.pronavGain = pronavGain;*/ + if (GuidanceMode == GuidanceModes.APN || GuidanceMode == GuidanceModes.PN) + ml.pronavGain = pronavGain; + + ml.terminalHoming = terminalHoming; + if (terminalHoming) + { + if (homingModeTerminal == GuidanceModes.AGMBallistic) + { + ml.BallisticOverShootFactor = BallisticOverShootFactor; //are some of these null, and causeing this to quit? + ml.BallisticAngle = BallisticAngle; + } + if (homingModeTerminal == GuidanceModes.Cruise) + { + ml.CruiseAltitude = CruiseAltitude; + ml.CruiseSpeed = CruiseSpeed; + ml.CruisePredictionTime = CruisePredictionTime; + } + if (homingModeTerminal == GuidanceModes.AAMLoft) + { + ml.LoftMaxAltitude = LoftMaxAltitude; + ml.LoftRangeOverride = LoftRangeOverride; + ml.LoftAltitudeAdvMax = LoftAltitudeAdvMax; + ml.LoftMinAltitude = LoftMinAltitude; + ml.LoftAngle = LoftAngle; + ml.LoftTermAngle = LoftTermAngle; + ml.LoftRangeFac = LoftRangeFac; + ml.LoftVelComp = LoftVelComp; + ml.LoftVertVelComp = LoftVertVelComp; + //ml.LoftAltComp = LoftAltComp; + ml.pronavGain = pronavGain; + ml.loftState = 0; + ml.TimeToImpact = float.PositiveInfinity; + ml.initMaxAoA = maxAoA; + } + if (homingModeTerminal == GuidanceModes.APN || homingModeTerminal == GuidanceModes.PN) + ml.pronavGain = pronavGain; + + ml.terminalHomingRange = terminalHomingRange; + ml.homingModeTerminal = homingModeTerminal; + ml.terminalHomingActive = false; + } + ml.decoupleForward = decoupleForward; + ml.decoupleSpeed = decoupleSpeed; + if (GuidanceMode == GuidanceModes.AGM) + ml.maxAltitude = maxAltitude; + ml.terminalGuidanceShouldActivate = terminalGuidanceShouldActivate; + ml.guidanceActive = true; + if (wpm != null) + { + ml.Team = wpm.Team; + wpm.SendTargetDataToMissile(ml); + wpm.heatTarget = TargetSignatureData.noTarget; + } + ml.TargetPosition = transform.position + (multiLauncher ? vessel.ReferenceTransform.up * 5000 : transform.forward * 5000); //set initial target position so if no target update, missileBase will count a miss if it nears this point or is flying post-thrust + ml.MissileLaunch(); + GetMissileCount(); + if (reloadableRail.ammoCount > 0 || BDArmorySettings.INFINITE_ORDINANCE) + { + if (!(reloadRoutine != null)) + { + reloadRoutine = StartCoroutine(MissileReload()); + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.MissileLauncher] reloading standard missile"); + } + } + reloadableMissile = null; + } + public void MissileLaunch() + { + HasFired = true; + try // FIXME Remove this once the fix is sufficiently tested. + { GameEvents.onPartDie.Add(PartDie); - BDATargetManager.FiredMissiles.Add(this); if (GetComponentInChildren()) { BDArmorySetup.numberOfParticleEmitters++; } - var wpm = VesselModuleRegistry.GetMissileFire(vessel, true); - if (wpm != null) Team = wpm.Team; - if (sfAudioSource == null) SetupAudio(); - sfAudioSource.PlayOneShot(GameDatabase.Instance.GetAudioClip("BDArmory/Sounds/deployClick")); - SourceVessel = vessel; + sfAudioSource.PlayOneShot(SoundUtils.GetAudioClip("BDArmory/Sounds/deployClick")); + //SourceVessel = vessel; //TARGETING - TargetPosition = transform.position + (transform.forward * 5000); //set initial target position so if no target update, missileBase will count a miss if it nears this point or is flying post-thrust startDirection = transform.forward; + if (maxAltitude == 0) // && GuidanceMode != GuidanceModes.Lofted) + { + if (targetVessel != null) maxAltitude = (float)Math.Max(vessel.radarAltitude, targetVessel.Vessel.radarAltitude) + 1000; + else maxAltitude = (float)vessel.radarAltitude + 2500; + } SetLaserTargeting(); SetAntiRadTargeting(); - part.decouple(0); part.force_activate(); - part.Unpack(); + vessel.situation = Vessel.Situations.FLYING; part.rb.isKinematic = false; part.bodyLiftMultiplier = 0; @@ -806,11 +1247,7 @@ public override void FireMissile() AddTargetInfoToVessel(); StartCoroutine(DecoupleRoutine()); - vessel.vesselName = GetShortName(); - vessel.vesselType = VesselType.Probe; - - TimeFired = Time.time; - + vessel.vesselType = VesselType.Probe; //setting ref transform for navball GameObject refObject = new GameObject(); refObject.transform.rotation = Quaternion.LookRotation(-transform.up, transform.forward); @@ -821,21 +1258,45 @@ public override void FireMissile() DetonationDistanceState = DetonationDistanceStates.NotSafe; MissileState = MissileStates.Drop; part.crashTolerance = 9999; //to combat stresses of launch, missle generate a lot of G Force + part.explosionPotential = 0; // Minimise the default part explosion FX that sometimes gets offset from the main explosion. StartCoroutine(MissileRoutine()); + if (multiLauncher && multiLauncher.isClusterMissile) + { + reloadableRail.MissileName = multiLauncher.subMunitionName; + reloadableRail.UpdateMissileValues(); + } + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.MissileLauncher]: Missile Launched!"); + if (BDArmorySettings.CAMERA_SWITCH_INCLUDE_MISSILES && SourceVessel.isActiveVessel) LoadedVesselSwitcher.Instance.ForceSwitchVessel(vessel); } catch (Exception e) { Debug.LogError("[BDArmory.MissileLauncher]: DEBUG " + e.Message); - try { Debug.LogError("[BDArmory.MissileLauncher]: DEBUG null part?: " + (part == null)); } catch (Exception e2) { Debug.LogError("[BDArmory.MissileLauncher]: DEBUG part: " + e2.Message); } - try { Debug.LogError("[BDArmory.MissileLauncher]: DEBUG null part.rb?: " + (part.rb == null)); } catch (Exception e2) { Debug.LogError("[BDArmory.MissileLauncher]: DEBUG part.rb: " + e2.Message); } - try { Debug.LogError("[BDArmory.MissileLauncher]: DEBUG null BDATargetManager.FiredMissiles?: " + (BDATargetManager.FiredMissiles == null)); } catch (Exception e2) { Debug.LogError("[BDArmory.MissileLauncher]: DEBUG BDATargetManager.FiredMissiles: " + e2.Message); } - try { Debug.LogError("[BDArmory.MissileLauncher]: DEBUG null vessel?: " + (vessel == null)); } catch (Exception e2) { Debug.LogError("[BDArmory.MissileLauncher]: DEBUG vessel: " + e2.Message); } - try { Debug.LogError("[BDArmory.MissileLauncher]: DEBUG null sfAudioSource?: " + (sfAudioSource == null)); } catch (Exception e2) { Debug.LogError("[BDArmory.MissileLauncher]: DEBUG sfAudioSource: " + e2.Message); } + try { Debug.LogWarning("[BDArmory.MissileLauncher]: DEBUG null part?: " + (part == null)); } catch (Exception e2) { Debug.LogWarning("[BDArmory.MissileLauncher]: DEBUG part: " + e2.Message); } + try { Debug.LogWarning("[BDArmory.MissileLauncher]: DEBUG null part.rb?: " + (part.rb == null)); } catch (Exception e2) { Debug.LogWarning("[BDArmory.MissileLauncher]: DEBUG part.rb: " + e2.Message); } + try { Debug.LogWarning("[BDArmory.MissileLauncher]: DEBUG null BDATargetManager.FiredMissiles?: " + (BDATargetManager.FiredMissiles == null)); } catch (Exception e2) { Debug.LogWarning("[BDArmory.MissileLauncher]: DEBUG BDATargetManager.FiredMissiles: " + e2.Message); } + try { Debug.LogWarning("[BDArmory.MissileLauncher]: DEBUG null vessel?: " + (vessel == null)); } catch (Exception e2) { Debug.LogWarning("[BDArmory.MissileLauncher]: DEBUG vessel: " + e2.Message); } + try { Debug.LogWarning("[BDArmory.MissileLauncher]: DEBUG null sfAudioSource?: " + (sfAudioSource == null)); } catch (Exception e2) { Debug.LogWarning("[BDArmory.MissileLauncher]: DEBUG sfAudioSource: " + e2.Message); } throw; // Re-throw the exception so behaviour is unchanged so we see it. } } + public IEnumerator MissileReload() + { + yield return new WaitForSecondsFixed(reloadableRail.reloadTime); + launched = false; + part.partTransform.localScale = origScale; + reloadTimer = 0; + gauge.UpdateReloadMeter(1); + if (!multiLauncher) part.crashTolerance = 5; + if (!inCargoBay) part.ShieldedFromAirstream = false; + if (deployableRail) deployableRail.UpdateChildrenPos(); + if (rotaryRail) rotaryRail.UpdateMissilePositions(); + if (multiLauncher) multiLauncher.PopulateMissileDummies(); + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileLauncher] reload complete on {part.name}"); + reloadRoutine = null; + } + IEnumerator DecoupleRoutine() { yield return new WaitForFixedUpdate(); @@ -848,6 +1309,11 @@ IEnumerator DecoupleRoutine() if (decoupleForward) { part.rb.velocity += decoupleSpeed * part.transform.forward; + if (multiLauncher && multiLauncher.isMultiLauncher && multiLauncher.salvoSize > 1) //add some scatter to missile salvoes + { + part.rb.velocity += (UnityEngine.Random.Range(-1f, 1f) * (decoupleSpeed / 4)) * part.transform.up; + part.rb.velocity += (UnityEngine.Random.Range(-1f, 1f) * (decoupleSpeed / 4)) * part.transform.right; + } } else { @@ -863,7 +1329,7 @@ public void FireMissileOnTarget(Vessel v) { if (!HasFired) { - legacyTargetVessel = v; + targetVessel = v.gameObject.GetComponent(); FireMissile(); } } @@ -878,6 +1344,12 @@ void OnDisable() public override void OnFixedUpdate() { + base.OnFixedUpdate(); + + if (!HighLogic.LoadedSceneIsFlight) return; + + FloatingOriginCorrection(); + try // FIXME Remove this once the fix is sufficiently tested. { debugString.Length = 0; @@ -886,10 +1358,8 @@ public override void OnFixedUpdate() { CheckDetonationState(); CheckDetonationDistance(); - part.rb.isKinematic = false; AntiSpin(); - //simpleDrag if (useSimpleDrag) { @@ -907,10 +1377,9 @@ public override void OnFixedUpdate() && Vector3.Angle(vessel.Velocity(), FlightGlobals.ActiveVessel.transform.position - transform.position) < 60) { if (sfAudioSource == null) SetupAudio(); - sfAudioSource.PlayOneShot(GameDatabase.Instance.GetAudioClip("BDArmory/Sounds/missileFlyby")); + sfAudioSource.PlayOneShot(SoundUtils.GetAudioClip("BDArmory/Sounds/missileFlyby")); hasPlayedFlyby = true; } - if (vessel.isActiveVessel) { audioSource.dopplerLevel = 0; @@ -919,14 +1388,13 @@ public override void OnFixedUpdate() { audioSource.dopplerLevel = 1f; } - if (TimeIndex > 0.5f) { if (torpedo) { if (vessel.altitude > 0) { - part.crashTolerance = waterImpactTolerance; + part.crashTolerance = waterImpactTolerance; // + (float)vessel.horizontalSrfSpeed; ? } else { @@ -941,6 +1409,7 @@ public override void OnFixedUpdate() UpdateThrustForces(); UpdateGuidance(); + //RaycastCollisions(); //Timed detonation @@ -952,16 +1421,49 @@ public override void OnFixedUpdate() } catch (Exception e) { - Debug.LogError("[BDArmory.MissileLauncher]: DEBUG " + e.Message); - try { Debug.LogError("[BDArmory.MissileLauncher]: DEBUG null part?: " + (part == null)); } catch (Exception e2) { Debug.LogError("[BDArmory.MissileLauncher]: DEBUG part: " + e2.Message); } - try { Debug.LogError("[BDArmory.MissileLauncher]: DEBUG null part.rb?: " + (part.rb == null)); } catch (Exception e2) { Debug.LogError("[BDArmory.MissileLauncher]: DEBUG part.rb: " + e2.Message); } - try { Debug.LogError("[BDArmory.MissileLauncher]: DEBUG null vessel?: " + (vessel == null)); } catch (Exception e2) { Debug.LogError("[BDArmory.MissileLauncher]: DEBUG vessel: " + e2.Message); } - try { Debug.LogError("[BDArmory.MissileLauncher]: DEBUG null audioSource?: " + (audioSource == null)); } catch (Exception e2) { Debug.LogError("[BDArmory.MissileLauncher]: DEBUG audioSource: " + e2.Message); } - try { Debug.LogError("[BDArmory.MissileLauncher]: DEBUG null sfAudioSource?: " + (sfAudioSource == null)); } catch (Exception e2) { Debug.LogError("[BDArmory.MissileLauncher]: sfAudioSource: " + e2.Message); } - try { Debug.LogError("[BDArmory.MissileLauncher]: DEBUG null FlightGlobals.ActiveVessel?: " + (FlightGlobals.ActiveVessel == null)); } catch (Exception e2) { Debug.LogError("[BDArmory.MissileLauncher]: DEBUG FlightGlobals.ActiveVessel: " + e2.Message); } - try { Debug.LogError("[BDArmory.MissileLauncher]: DEBUG null FlightCamera.fetch?: " + (FlightCamera.fetch == null)); } catch (Exception e2) { Debug.LogError("[BDArmory.MissileLauncher]: DEBUG FlightCamera.fetch: " + e2.Message); } - try { Debug.LogError("[BDArmory.MissileLauncher]: DEBUG null FlightCamera.fetch.mainCamera?: " + (FlightCamera.fetch.mainCamera == null)); } catch (Exception e2) { Debug.LogError("[BDArmory.MissileLauncher]: DEBUG FlightCamera.fetch.mainCamera: " + e2.Message); } - throw; // Re-throw the exception so behaviour is unchanged so we see it. + Debug.LogError("[BDArmory.MissileLauncher]: DEBUG " + e.Message + "\n" + e.StackTrace); + // throw; // Re-throw the exception so behaviour is unchanged so we see it. + /* FIXME this is being caused by attempting to get the wm.Team in RadarUpdateMissileLock. A similar exception occurred in BDATeamIcons, line 239 + [ERR 12:05:24.391] Module MissileLauncher threw during OnFixedUpdate: System.NullReferenceException: Object reference not set to an instance of an object + at BDArmory.Radar.RadarUtils.RadarUpdateMissileLock (UnityEngine.Ray ray, System.Single fov, BDArmory.Targeting.TargetSignatureData[]& dataArray, System.Single dataPersistTime, BDArmory.Weapons.Missiles.MissileBase missile) [0x00076] in /storage/github/BDArmory/BDArmory/Radar/RadarUtils.cs:972 + at BDArmory.Weapons.Missiles.MissileBase.UpdateRadarTarget () [0x003d9] in /storage/github/BDArmory/BDArmory/Weapons/Missiles/MissileBase.cs:747 + at BDArmory.Weapons.Missiles.MissileLauncher.UpdateGuidance () [0x000ba] in /storage/github/BDArmory/BDArmory/Weapons/Missiles/MissileLauncher.cs:1134 + at BDArmory.Weapons.Missiles.MissileLauncher.OnFixedUpdate () [0x00593] in /storage/github/BDArmory/BDArmory/Weapons/Missiles/MissileLauncher.cs:1046 + at Part.ModulesOnFixedUpdate () [0x000bd] in <4deecb19beb547f19b1ff89b4c59bd84>:0 + UnityEngine.DebugLogHandler:LogFormat(LogType, Object, String, Object[]) + ModuleManager.UnityLogHandle.InterceptLogHandler:LogFormat(LogType, Object, String, Object[]) + UnityEngine.Debug:LogError(Object) + Part:ModulesOnFixedUpdate() + Part:FixedUpdate() + */ + } + if (reloadableRail) + { + if (launched && reloadRoutine != null) + { + reloadTimer = Mathf.Clamp((reloadTimer + 1 * TimeWarp.fixedDeltaTime / reloadableRail.reloadTime), 0, 1); + if (vessel.isActiveVessel) gauge.UpdateReloadMeter(reloadTimer); + } + if (heatTimer > 0) + { + heatTimer -= TimeWarp.fixedDeltaTime; + if (vessel.isActiveVessel) + { + gauge.UpdateHeatMeter(heatTimer / multiLauncher.launcherCooldown); + } + } + if (OldInfAmmo != BDArmorySettings.INFINITE_ORDINANCE) + { + if (reloadableRail.ammoCount < 1 && BDArmorySettings.INFINITE_ORDINANCE) + { + if (!(reloadRoutine != null)) + { + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.MissileLauncher] Infinite Ammo enabled, reloading"); + reloadRoutine = StartCoroutine(MissileReload()); + } + } + OldInfAmmo = BDArmorySettings.INFINITE_ORDINANCE; + } } } @@ -985,10 +1487,10 @@ private void CheckMiss() bool targetBehindMissile = Vector3.Dot(TargetPosition - transform.position, transform.forward) < 0f; if ((pastGracePeriod && targetBehindMissile) || noProgress) // Check that we're not moving away from the target after a grace period { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.MissileLauncher]: Missile has missed!"); + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.MissileLauncher]: Missile has missed!"); if (vessel.altitude >= maxAltitude && maxAltitude != 0f) - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.MissileLauncher]: CheckMiss trigged by MaxAltitude"); + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.MissileLauncher]: CheckMiss trigged by MaxAltitude"); HasMissed = true; guidanceActive = false; @@ -1001,50 +1503,80 @@ private void CheckMiss() if (launcher.hasRCS) launcher.KillRCS(); } - if (sqrDist < Mathf.Pow(GetBlastRadius() * 0.5f, 2)) part.Destroy(); + var distThreshold = 0.5f * GetBlastRadius(); + if (sqrDist < distThreshold * distThreshold) part.Destroy(); + if (FuseFailed) part.Destroy(); isTimed = true; detonationTime = TimeIndex + 1.5f; + if (BDArmorySettings.CAMERA_SWITCH_INCLUDE_MISSILES && vessel.isActiveVessel) LoadedVesselSwitcher.Instance.TriggerSwitchVessel(); return; } } } + string debugGuidanceTarget; void UpdateGuidance() { - string debugTarget = "none"; - if (guidanceActive) - { - if (TargetingMode == TargetingModes.Heat) - { - UpdateHeatTarget(); - if (heatTarget.vessel) - debugTarget = heatTarget.vessel.GetDisplayName() + " " + heatTarget.signalStrength.ToString(); - else if (heatTarget.signalStrength > 0) - debugTarget = "Flare " + heatTarget.signalStrength.ToString(); - } - else if (TargetingMode == TargetingModes.Radar) - { - UpdateRadarTarget(); - if (radarTarget.vessel) - debugTarget = radarTarget.vessel.GetDisplayName() + " " + radarTarget.signalStrength.ToString(); - else if (radarTarget.signalStrength > 0) - debugTarget = "Chaff " + radarTarget.signalStrength.ToString(); - } - else if (TargetingMode == TargetingModes.Laser) + if (guidanceActive && guidanceFailureRatePerFrame > 0f) + if (UnityEngine.Random.Range(0f, 1f) < guidanceFailureRatePerFrame) { - UpdateLaserTarget(); - debugTarget = TargetPosition.ToString(); - } - else if (TargetingMode == TargetingModes.Gps) - { - UpdateGPSTarget(); - debugTarget = UpdateGPSTarget().ToString(); + guidanceActive = false; + BDATargetManager.FiredMissiles.Remove(this); + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.MissileLauncher]: Missile Guidance Failed!"); } - else if (TargetingMode == TargetingModes.AntiRad) + + if (guidanceActive) + { + switch (TargetingMode) { - UpdateAntiRadiationTarget(); - debugTarget = TargetPosition.ToString(); + case TargetingModes.Heat: + UpdateHeatTarget(); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_MISSILES) + { + if (heatTarget.vessel) + debugGuidanceTarget = $"{heatTarget.vessel.GetDisplayName()} {heatTarget.signalStrength}"; + else if (heatTarget.signalStrength > 0) + debugGuidanceTarget = $"Flare {heatTarget.signalStrength}"; + } + break; + case TargetingModes.Radar: + UpdateRadarTarget(); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_MISSILES) + { + if (radarTarget.vessel) + debugGuidanceTarget = $"{radarTarget.vessel.GetDisplayName()} {radarTarget.signalStrength}"; + else if (radarTarget.signalStrength > 0) + debugGuidanceTarget = $"Chaff {radarTarget.signalStrength}"; + } + break; + case TargetingModes.Laser: + UpdateLaserTarget(); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_MISSILES) + { + debugGuidanceTarget = TargetPosition.ToString(); + } + break; + case TargetingModes.Gps: + UpdateGPSTarget(); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_MISSILES) + { + debugGuidanceTarget = UpdateGPSTarget().ToString(); + } + break; + case TargetingModes.AntiRad: + UpdateAntiRadiationTarget(); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_MISSILES) + { + debugGuidanceTarget = TargetPosition.ToString(); + } + break; + default: + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_MISSILES) + { + debugGuidanceTarget = "none"; + } + break; } UpdateTerminalGuidance(); @@ -1070,18 +1602,17 @@ void UpdateGuidance() controlAuthority = 1; } - debugString.Append($"controlAuthority: {controlAuthority}"); - debugString.Append(Environment.NewLine); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_MISSILES) debugString.AppendLine($"controlAuthority: {controlAuthority}"); if (guidanceActive)// && timeIndex - dropTime > 0.5f) { WarnTarget(); - if (legacyTargetVessel && legacyTargetVessel.loaded) - { - Vector3 targetCoMPos = legacyTargetVessel.CoM; - TargetPosition = targetCoMPos + legacyTargetVessel.Velocity() * Time.fixedDeltaTime; - } + //if (targetVessel && targetVessel.loaded) + //{ + // Vector3 targetCoMPos = targetVessel.CoM; + // TargetPosition = targetCoMPos + targetVessel.Velocity() * Time.fixedDeltaTime; + //} //increaseTurnRate after launch float turnRateDPS = Mathf.Clamp(((TimeIndex - dropTime) / boostTime) * maxTurnRateDPS * 25f, 0, maxTurnRateDPS); @@ -1123,7 +1654,17 @@ void UpdateGuidance() finalMaxTorque = Mathf.Clamp((TimeIndex - dropTime) * torqueRampUp, 0, maxTorque); //ramp up torque - if (GuidanceMode == GuidanceModes.AAMLead) + if (terminalHoming && !terminalHomingActive) + { + if (Vector3.SqrMagnitude(TargetPosition - vessel.transform.position) < terminalHomingRange * terminalHomingRange) + { + GuidanceMode = homingModeTerminal; + terminalHomingActive = true; + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.MissileGuidance]: Terminal"); + } + } + + if ((GuidanceMode == GuidanceModes.AAMLead) || (GuidanceMode == GuidanceModes.APN) || (GuidanceMode == GuidanceModes.PN) || (GuidanceMode == GuidanceModes.AAMLoft) || (GuidanceMode == GuidanceModes.AAMPure) )// || (GuidanceMode == GuidanceModes.AAMHybrid)) { AAMGuidance(); } @@ -1174,8 +1715,7 @@ void UpdateGuidance() } } - debugString.Append("Missile target=" + debugTarget); - debugString.Append(Environment.NewLine); + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_MISSILES) debugString.AppendLine("Missile target=" + debugGuidanceTarget); } // feature_engagementenvelope: terminal guidance mode for cruise missiles @@ -1186,7 +1726,7 @@ private void UpdateTerminalGuidance() if (terminalGuidanceShouldActivate && !terminalGuidanceActive && (TargetingModeTerminal != TargetingModes.None) && (distanceSqr < terminalGuidanceDistance * terminalGuidanceDistance)) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.MissileLauncher][Terminal Guidance]: missile " + this.name + " updating targeting mode: " + terminalGuidanceType); + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileLauncher][Terminal Guidance]: missile {GetPartName()} updating targeting mode: {terminalGuidanceType}"); TargetingMode = TargetingModeTerminal; terminalGuidanceActive = true; @@ -1196,32 +1736,37 @@ private void UpdateTerminalGuidance() { case TargetingModes.Heat: // gets ground heat targets and after locking one, disallows the lock to break to another target - heatTarget = BDATargetManager.GetHeatTarget(SourceVessel, vessel, new Ray(transform.position + (50 * GetForwardTransform()), GetForwardTransform()), heatTarget, terminalGuidanceDistance, heatThreshold, true, lockedSensorFOVBias, lockedSensorVelocityBias, SourceVessel ? VesselModuleRegistry.GetModule(SourceVessel) : null, true); + heatTarget = BDATargetManager.GetHeatTarget(SourceVessel, vessel, new Ray(transform.position + (50 * GetForwardTransform()), GetForwardTransform()), heatTarget, lockedSensorFOV / 2, heatThreshold, frontAspectHeatModifier, uncagedLock, lockedSensorFOVBias, lockedSensorVelocityBias, SourceVessel ? VesselModuleRegistry.GetModule(SourceVessel) : null, targetVessel); if (heatTarget.exists) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_MISSILES) { - Debug.Log("[BDArmory.MissileLauncher][Terminal Guidance]: Heat target acquired! Position: " + heatTarget.position + ", heatscore: " + heatTarget.signalStrength); + Debug.Log($"[BDArmory.MissileLauncher][Terminal Guidance]: Heat target acquired! Position: {heatTarget.position}, heatscore: {heatTarget.signalStrength}"); } TargetAcquired = true; - TargetPosition = heatTarget.position + (heatTarget.velocity * Time.fixedDeltaTime); + TargetPosition = heatTarget.position + (2 * heatTarget.velocity * Time.fixedDeltaTime); // Not sure why this is 2* TargetVelocity = heatTarget.velocity; TargetAcceleration = heatTarget.acceleration; - lockFailTimer = 0; - targetGPSCoords = VectorUtils.WorldPositionToGeoCoords(TargetPosition, vessel.mainBody); + lockFailTimer = -1; // ensures proper entry into UpdateHeatTarget() + + // Disable terminal guidance and switch to regular heat guidance for next update + terminalGuidanceShouldActivate = false; + TargetingMode = TargetingModes.Heat; // Adjust heat score based on distance missile will travel in the next update if (heatTarget.signalStrength > 0) { float currentFactor = (1400 * 1400) / Mathf.Clamp((heatTarget.position - transform.position).sqrMagnitude, 90000, 36000000); Vector3 currVel = (float)vessel.srfSpeed * vessel.Velocity().normalized; - float futureFactor = (1400 * 1400) / Mathf.Clamp((TargetPosition - (transform.position + (currVel * Time.fixedDeltaTime))).sqrMagnitude, 90000, 36000000); + heatTarget.position = heatTarget.position + heatTarget.velocity * Time.fixedDeltaTime; + heatTarget.velocity = heatTarget.velocity + heatTarget.acceleration * Time.fixedDeltaTime; + float futureFactor = (1400 * 1400) / Mathf.Clamp((heatTarget.position - (transform.position + (currVel * Time.fixedDeltaTime))).sqrMagnitude, 90000, 36000000); heatTarget.signalStrength *= futureFactor / currentFactor; } } else { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_MISSILES) { Debug.Log("[BDArmory.MissileLauncher][Terminal Guidance]: Missile heatseeker could not acquire a target lock."); } @@ -1237,7 +1782,7 @@ private void UpdateTerminalGuidance() //RadarUtils.UpdateRadarLock(ray, maxOffBoresight, activeRadarMinThresh, ref scannedTargets, 0.4f, true, RadarWarningReceiver.RWRThreatTypes.MissileLock, true); RadarUtils.RadarUpdateMissileLock(ray, maxOffBoresight, ref scannedTargets, 0.4f, this); - float sqrThresh = Mathf.Pow(terminalGuidanceDistance * 1.5f, 2); + float sqrThresh = terminalGuidanceDistance * terminalGuidanceDistance * 2.25f; // (terminalGuidanceDistance * 1.5f)^2 //float smallestAngle = maxOffBoresight; TargetSignatureData lockedTarget = TargetSignatureData.noTarget; @@ -1264,7 +1809,7 @@ private void UpdateTerminalGuidance() TargetPosition = radarTarget.predictedPosition; } else - TargetPosition = radarTarget.predictedPositionWithChaffFactor; + TargetPosition = radarTarget.predictedPositionWithChaffFactor(chaffEffectivity); TargetVelocity = radarTarget.velocity; TargetAcceleration = radarTarget.acceleration; targetGPSCoords = VectorUtils.WorldPositionToGeoCoords(TargetPosition, vessel.mainBody); @@ -1274,7 +1819,7 @@ private void UpdateTerminalGuidance() else RadarWarningReceiver.PingRWR(new Ray(transform.position, radarTarget.predictedPosition - transform.position), 45, RadarWarningReceiver.RWRThreatTypes.MissileLaunch, 2f); - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.MissileLauncher][Terminal Guidance]: Pitbull! Radar missileBase has gone active. Radar sig strength: " + radarTarget.signalStrength.ToString("0.0") + " - target: " + radarTarget.vessel.name); + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileLauncher][Terminal Guidance]: Pitbull! Radar missileBase has gone active. Radar sig strength: {radarTarget.signalStrength:0.0} - target: {radarTarget.vessel.name}"); } else { @@ -1283,7 +1828,7 @@ private void UpdateTerminalGuidance() TargetVelocity = Vector3.zero; TargetAcceleration = Vector3.zero; targetGPSCoords = VectorUtils.WorldPositionToGeoCoords(TargetPosition, vessel.mainBody); - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.MissileLauncher][Terminal Guidance]: Missile radar could not acquire a target lock - Defaulting to GPS Target"); + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.MissileLauncher][Terminal Guidance]: Missile radar could not acquire a target lock - Defaulting to GPS Target"); } break; @@ -1299,7 +1844,7 @@ private void UpdateTerminalGuidance() TargetAcquired = true; targetGPSCoords = VectorUtils.WorldPositionToGeoCoords(TargetPosition, vessel.mainBody); // Set the GPS coordinates from the current target position. SetAntiRadTargeting(); //should then already work automatically via OnReceiveRadarPing - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.MissileLauncher][Terminal Guidance]: Antiradiation mode set! Waiting for radar signals..."); + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.MissileLauncher][Terminal Guidance]: Antiradiation mode set! Waiting for radar signals..."); break; } } @@ -1311,9 +1856,7 @@ void UpdateThrustForces() if (weaponClass == WeaponClasses.SLW && FlightGlobals.getAltitudeAtPos(part.transform.position) > 0) return; //#710, no torp thrust out of water if (currentThrust * Throttle > 0) { - debugString.Append("Missile thrust=" + currentThrust * Throttle); - debugString.Append(Environment.NewLine); - + if (BDArmorySettings.DEBUG_TELEMETRY || BDArmorySettings.DEBUG_MISSILES) debugString.AppendLine($"Missile thrust= {currentThrust * Throttle}"); part.rb.AddRelativeForce(currentThrust * Throttle * Vector3.forward); } } @@ -1321,20 +1864,30 @@ void UpdateThrustForces() IEnumerator MissileRoutine() { MissileState = MissileStates.Drop; + if (engineFailureRate > 0f) + if (UnityEngine.Random.Range(0f, 1f) < engineFailureRate) + { + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.MissileLauncher]: Missile Engine Failed on Launch!"); + yield return new WaitForSecondsFixed(2f); // Pilot reaction time + BDATargetManager.FiredMissiles.Remove(this); + yield break; + } + StartCoroutine(DeployAnimRoutine()); - yield return new WaitForSeconds(dropTime); + yield return new WaitForSecondsFixed(dropTime); yield return StartCoroutine(BoostRoutine()); + StartCoroutine(FlightAnimRoutine()); - yield return new WaitForSeconds(cruiseDelay); + yield return new WaitForSecondsFixed(cruiseDelay); yield return StartCoroutine(CruiseRoutine()); } IEnumerator DeployAnimRoutine() { - yield return new WaitForSeconds(deployTime); + yield return new WaitForSecondsFixed(deployTime); if (deployStates == null) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.LogWarning("[BDArmory.MissileLauncher]: deployStates was null, aborting AnimRoutine."); + if (BDArmorySettings.DEBUG_MISSILES) Debug.LogWarning("[BDArmory.MissileLauncher]: deployStates was null, aborting AnimRoutine."); yield break; } @@ -1345,6 +1898,7 @@ IEnumerator DeployAnimRoutine() while (anim.MoveNext()) { if (anim.Current == null) continue; + anim.Current.enabled = true; anim.Current.speed = 1; } } @@ -1353,7 +1907,7 @@ IEnumerator FlightAnimRoutine() { if (animStates == null) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.LogWarning("[BDArmory.MissileLauncher]: animStates was null, aborting AnimRoutine."); + if (BDArmorySettings.DEBUG_MISSILES) Debug.LogWarning("[BDArmory.MissileLauncher]: animStates was null, aborting AnimRoutine."); yield break; } @@ -1363,6 +1917,7 @@ IEnumerator FlightAnimRoutine() while (anim.MoveNext()) { if (anim.Current == null) continue; + anim.Current.enabled = true; if (!OneShotAnim) { anim.Current.wrapMode = WrapMode.Loop; @@ -1373,7 +1928,15 @@ IEnumerator FlightAnimRoutine() } IEnumerator BoostRoutine() { + if (weaponClass == WeaponClasses.SLW && FlightGlobals.getAltitudeAtPos(part.transform.position) > 0) + { + yield return new WaitUntilFixed(() => vessel == null || vessel.LandedOrSplashed);//don't start torpedo thrust until underwater + if (vessel == null || vessel.Landed) Detonate(); //dropping torpedoes over land is just going to turn them into heavy, expensive bombs... + dropTime = TimeIndex; + } + if (useFuel) burnRate = boostTime > 0 ? boosterFuelMass / boostTime * Time.fixedDeltaTime : 0; StartBoost(); + var wait = new WaitForFixedUpdate(); float boostStartTime = Time.time; while (Time.time - boostStartTime < boostTime) { @@ -1418,12 +1981,17 @@ IEnumerator BoostRoutine() } //thrust + if (useFuel && burnRate > 0 && burnedFuelMass < boosterFuelMass) + { + burnedFuelMass = Mathf.Min(burnedFuelMass + burnRate, boosterFuelMass); + } + if (spoolEngine) { currentThrust = Mathf.MoveTowards(currentThrust, thrust, thrust / 10); } - yield return null; + yield return wait; } EndBoost(); } @@ -1473,8 +2041,8 @@ void StartBoost() } if (!(thrust > 0)) return; - sfAudioSource.PlayOneShot(GameDatabase.Instance.GetAudioClip("BDArmory/Sounds/launch")); - RadarWarningReceiver.WarnMissileLaunch(transform.position, transform.forward); + sfAudioSource.PlayOneShot(SoundUtils.GetAudioClip("BDArmory/Sounds/launch")); + RadarWarningReceiver.WarnMissileLaunch(transform.position, transform.forward, TargetingMode == TargetingModes.Radar); } void EndBoost() @@ -1493,6 +2061,8 @@ void EndBoost() gEmitter.Current.emit = false; } + if (useFuel) burnedFuelMass = boosterFuelMass; + if (decoupleBoosters) { part.mass -= boosterMass; @@ -1512,7 +2082,14 @@ void EndBoost() IEnumerator CruiseRoutine() { + float massToBurn = 0; + if (useFuel) + { + burnRate = cruiseTime > 0 ? cruiseFuelMass / cruiseTime * Time.fixedDeltaTime : 0; + massToBurn = boosterFuelMass + cruiseFuelMass; + } StartCruise(); + var wait = new WaitForFixedUpdate(); float cruiseStartTime = Time.time; while (Time.time - cruiseStartTime < cruiseTime) { @@ -1566,12 +2143,17 @@ IEnumerator CruiseRoutine() gpe.Current.emit = false; } } + //Thrust + if (useFuel && burnRate > 0 && burnedFuelMass < massToBurn) + { + burnedFuelMass = Mathf.Min(burnedFuelMass + burnRate, massToBurn); + } if (spoolEngine) { currentThrust = Mathf.MoveTowards(currentThrust, cruiseThrust, cruiseThrust / 10); } - yield return null; + yield return wait; } EndCruise(); } @@ -1613,13 +2195,14 @@ void EndCruise() { MissileState = MissileStates.PostThrust; - IEnumerator light = gameObject.GetComponentsInChildren().AsEnumerable().GetEnumerator(); - while (light.MoveNext()) - { - if (light.Current == null) continue; - light.Current.intensity = 0; - } - light.Dispose(); + if (useFuel) burnedFuelMass = cruiseFuelMass + boosterFuelMass; + + using (IEnumerator light = gameObject.GetComponentsInChildren().AsEnumerable().GetEnumerator()) + while (light.MoveNext()) + { + if (light.Current == null) continue; + light.Current.intensity = 0; + } StartCoroutine(FadeOutAudio()); StartCoroutine(FadeOutEmitters()); @@ -1725,7 +2308,7 @@ void CruiseGuidance() Quaternion originalRTrotation = rotationTransform.rotation; transform.rotation = Quaternion.LookRotation(transform.forward, upDirection); rotationTransform.rotation = originalRTrotation; - Vector3 lookUpDirection = Vector3.ProjectOnPlane(cruiseTarget - transform.position, transform.forward) * 100; + Vector3 lookUpDirection = (cruiseTarget - transform.position).ProjectOnPlanePreNormalized(transform.forward) * 100; lookUpDirection = transform.InverseTransformPoint(lookUpDirection + transform.position); lookUpDirection = new Vector3(lookUpDirection.x, 0, 0); @@ -1747,19 +2330,67 @@ void AAMGuidance() Vector3 aamTarget; if (TargetAcquired) { + if (warheadType == WarheadTypes.ContinuousRod) //Have CR missiles target slightly above target to ensure craft caught in planar blast AOE + { + TargetPosition += VectorUtils.GetUpDirection(TargetPosition) * (blastRadius > 0 ? (DetonationDistance / 3) : 5); + //TargetPosition += VectorUtils.GetUpDirection(TargetPosition) * (blastRadius < 10? (blastRadius / 2) : 10); + } DrawDebugLine(transform.position + (part.rb.velocity * Time.fixedDeltaTime), TargetPosition); + float timeToImpact; - aamTarget = MissileGuidance.GetAirToAirTarget(TargetPosition, TargetVelocity, TargetAcceleration, vessel, out timeToImpact, optimumAirspeed); - TimeToImpact = timeToImpact; + if (GuidanceMode == GuidanceModes.APN) // Augmented Pro-Nav + aamTarget = MissileGuidance.GetAPNTarget(TargetPosition, TargetVelocity, TargetAcceleration, vessel, pronavGain, out timeToImpact); + else if (GuidanceMode == GuidanceModes.PN) // Pro-Nav + aamTarget = MissileGuidance.GetPNTarget(TargetPosition, TargetVelocity, vessel, pronavGain, out timeToImpact); + else if (GuidanceMode == GuidanceModes.AAMLoft) + { + float targetAlt = FlightGlobals.getAltitudeAtPos(TargetPosition); + + if (TimeToImpact == float.PositiveInfinity) + { + // If the missile is not in a vaccuum, is above LoftMinAltitude and has an angle to target below the climb angle (or 90 - climb angle if climb angle > 45) (in this case, since it's angle from the vertical the check is if it's > 90f - LoftAngle) and is either is at a lower altitude than targetAlt + LoftAltitudeAdvMax or further than LoftRangeOverride, then loft. + if (!vessel.InVacuum() && (vessel.altitude >= LoftMinAltitude) && Vector3.Angle(TargetPosition - vessel.transform.position, VectorUtils.GetUpDirection(vessel.CoM)) > Mathf.Min(LoftAngle, 90f - LoftAngle) && ((vessel.altitude - targetAlt <= LoftAltitudeAdvMax) || (TargetPosition - vessel.transform.position).sqrMagnitude > (LoftRangeOverride * LoftRangeOverride))) loftState = 0; + else loftState = 3; + } + + //aamTarget = MissileGuidance.GetAirToAirLoftTarget(TargetPosition, TargetVelocity, TargetAcceleration, vessel, targetAlt, LoftMaxAltitude, LoftRangeFac, LoftAltComp, LoftVelComp, LoftAngle, LoftTermAngle, terminalHomingRange, ref loftState, out float currTimeToImpact, out float rangeToTarget, optimumAirspeed); + aamTarget = MissileGuidance.GetAirToAirLoftTarget(TargetPosition, TargetVelocity, TargetAcceleration, vessel, targetAlt, LoftMaxAltitude, LoftRangeFac, LoftVertVelComp, LoftVelComp, LoftAngle, LoftTermAngle, terminalHomingRange, ref loftState, out float currTimeToImpact, out float rangeToTarget, homingModeTerminal, pronavGain, optimumAirspeed); + + float fac = (1 - (rangeToTarget - terminalHomingRange) / Mathf.Clamp(terminalHomingRange * 4f, 5000f, 25000f)); + + maxAoA = Mathf.Clamp(initMaxAoA * fac, 4f, initMaxAoA); + + TimeToImpact = currTimeToImpact; + + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileLauncher]: AAM Loft TTGO: [{TimeToImpact:G3}]. Currently State: {loftState}. Fly to: [{aamTarget}]. Target Position: [{TargetPosition}]. Max AoA: [{maxAoA:G3}]"); + } + else if (GuidanceMode == GuidanceModes.AAMPure) + { + TimeToImpact = Vector3.Distance(TargetPosition, transform.position) / Mathf.Max((float)vessel.srfSpeed, optimumAirspeed); + aamTarget = TargetPosition; + } + /*else if (GuidanceMode == GuidanceModes.AAMHybrid) + { + aamTarget = MissileGuidance.GetAirToAirHybridTarget(TargetPosition, TargetVelocity, TargetAcceleration, vessel, terminalHomingRange, out timeToImpact, homingModeTerminal, pronavGain, optimumAirspeed); + TimeToImpact = timeToImpact; + }*/ + else// AAM Lead + aamTarget = MissileGuidance.GetAirToAirTarget(TargetPosition, TargetVelocity, TargetAcceleration, vessel, out timeToImpact, optimumAirspeed); + + if (Vector3.Angle(aamTarget - transform.position, transform.forward) > maxOffBoresight * 0.75f) { aamTarget = TargetPosition; } //proxy detonation - if (proxyDetonate && ((TargetPosition + (TargetVelocity * Time.fixedDeltaTime)) - (transform.position)).sqrMagnitude < Mathf.Pow(GetBlastRadius() * 0.5f, 2)) + var distThreshold = 0.5f * GetBlastRadius(); + if (proxyDetonate && !DetonateAtMinimumDistance && ((TargetPosition + (TargetVelocity * Time.fixedDeltaTime)) - (transform.position)).sqrMagnitude < distThreshold * distThreshold) { - part.Destroy(); + //part.Destroy(); //^look into how this interacts with MissileBase.DetonationState + // - if the missile is still within the notSafe status, the missile will delete itself, else, the checkProximity state of DetonationState would trigger before the missile reaches the 1/2 blastradius. + // would only trigger if someone set the detonation distance override to something smallerthan 1/2 blst radius, for some reason + Detonate(); } } else @@ -1786,7 +2417,7 @@ void AGMGuidance() if (targetViewAngle > maxOffBoresight) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.MissileLauncher]: AGM Missile guidance failed - target out of view"); + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.MissileLauncher]: AGM Missile guidance failed - target out of view"); guidanceActive = false; } CheckMiss(); @@ -1820,9 +2451,10 @@ void SLWGuidance() } //proxy detonation - if (proxyDetonate && ((TargetPosition + (TargetVelocity * Time.fixedDeltaTime)) - (transform.position)).sqrMagnitude < Mathf.Pow(GetBlastRadius() * 0.5f, 2)) + var distThreshold = 0.5f * GetBlastRadius(); + if (proxyDetonate && !DetonateAtMinimumDistance && ((TargetPosition + (TargetVelocity * Time.fixedDeltaTime)) - (transform.position)).sqrMagnitude < distThreshold * distThreshold) { - part.Destroy(); + Detonate(); //ends up the same as part.Destroy, except it doesn't trip the hasDied flag for clustermissiles } } else @@ -1853,69 +2485,105 @@ void AGMBallisticGuidance() public override void Detonate() { - if (HasExploded || !HasFired) return; + if (HasExploded || FuseFailed || !HasFired) return; - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.MissileLauncher]: Detonate Triggered"); + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.MissileLauncher]: Detonate Triggered"); BDArmorySetup.numberOfParticleEmitters--; HasExploded = true; - - if (legacyTargetVessel != null) + /* + if (targetVessel != null) { - using (var wpm = VesselModuleRegistry.GetModules(legacyTargetVessel).GetEnumerator()) + using (var wpm = VesselModuleRegistry.GetModules(targetVessel).GetEnumerator()) while (wpm.MoveNext()) { if (wpm.Current == null) continue; - wpm.Current.missileIsIncoming = false; + wpm.Current.missileIsIncoming = false; //handled by attacked vessel } } - + */ if (SourceVessel == null) SourceVessel = vessel; - - if (part.FindModuleImplementing() != null) - { - part.FindModuleImplementing().DetonateIfPossible(); - } - else if (part.FindModuleImplementing() != null) + if (multiLauncher && multiLauncher.isClusterMissile) { - part.FindModuleImplementing().Detonate(); + if (!HasDied) + { + if (fairings.Count > 0) + { + using (var fairing = fairings.GetEnumerator()) + while (fairing.MoveNext()) + { + if (fairing.Current == null) continue; + fairing.Current.AddComponent().DecoupleBooster(part.rb.velocity, boosterDecoupleSpeed); + } + } + reloadableRail.MissileName = multiLauncher.subMunitionName; + multiLauncher.Team = Team; + multiLauncher.fireMissile(true); + } } - else //TODO: Remove this backguard compatibility + else { - Vector3 position = transform.position;//+rigidbody.velocity*Time.fixedDeltaTime; + if (warheadType == WarheadTypes.Standard || warheadType == WarheadTypes.ContinuousRod) + { + var tnt = part.FindModuleImplementing(); + tnt.sourcevessel = SourceVessel; + tnt.DetonateIfPossible(); + FuseFailed = tnt.fuseFailed; + guidanceActive = false; + if (FuseFailed) + HasExploded = false; + } + else if (warheadType == WarheadTypes.Nuke) + { + var U235 = part.FindModuleImplementing(); + U235.Detonate(); + } + else // EMP/really ond legacy missiles using BlastPower + { + Vector3 position = transform.position;//+rigidbody.velocity*Time.fixedDeltaTime; - ExplosionFx.CreateExplosion(position, blastPower, explModelPath, explSoundPath, ExplosionSourceType.Missile, 0, part, SourceVessel.vesselName, part.FindModuleImplementing().GetShortName(), default(Vector3), -1, false, part.mass * 1000); + ExplosionFx.CreateExplosion(position, blastPower, explModelPath, explSoundPath, ExplosionSourceType.Missile, 0, part, SourceVessel.vesselName, GetShortName(), default(Vector3), -1, false, part.mass * 1000); + } + if (part != null && !FuseFailed) + { + DestroyMissile(); //splitting this off to a separate function so the clustermissile MultimissileLaunch can call it when the MML launch ienumerator is done + } } - List.Enumerator e = gaplessEmitters.GetEnumerator(); - while (e.MoveNext()) - { - if (e.Current == null) continue; - e.Current.gameObject.AddComponent(); - e.Current.transform.parent = null; - if (e.Current.GetComponent()) + using (var e = gaplessEmitters.GetEnumerator()) + while (e.MoveNext()) { - e.Current.GetComponent().enabled = false; + if (e.Current == null) continue; + e.Current.gameObject.AddComponent(); + e.Current.transform.parent = null; } - } - e.Dispose(); + using (IEnumerator light = gameObject.GetComponentsInChildren().AsEnumerable().GetEnumerator()) + while (light.MoveNext()) + { + if (light.Current == null) continue; + light.Current.intensity = 0; + } + } - if (part != null) - { - part.Destroy(); - part.explode(); - } + public void DestroyMissile() + { + part.Destroy(); + part.explode(); } public override Vector3 GetForwardTransform() { - return MissileReferenceTransform.forward; + if (multiLauncher && multiLauncher.overrideReferenceTransform) + return vessel.ReferenceTransform.up; + else + return MissileReferenceTransform.forward; } protected override void PartDie(Part p) { if (p == part) { + HasDied = true; Detonate(); BDATargetManager.FiredMissiles.Remove(this); GameEvents.onPartDie.Remove(PartDie); @@ -1929,9 +2597,9 @@ public static bool CheckIfMissile(Part p) void WarnTarget() { - if (legacyTargetVessel == null) return; - var wpm = VesselModuleRegistry.GetMissileFire(legacyTargetVessel, true); - if (wpm != null) wpm.MissileWarning(Vector3.Distance(transform.position, legacyTargetVessel.transform.position), this); + if (targetVessel == null) return; + var wpm = VesselModuleRegistry.GetMissileFire(targetVessel.Vessel, true); + if (wpm != null) wpm.MissileWarning(Vector3.Distance(transform.position, targetVessel.transform.position), this); } void SetupRCS() @@ -1958,7 +2626,7 @@ void DoRCS() if (Time.time - rcsFiredTimes[i] > rcsAudioMinInterval) { if (sfAudioSource == null) SetupAudio(); - sfAudioSource.PlayOneShot(GameDatabase.Instance.GetAudioClip("BDArmory/Sounds/popThrust")); + sfAudioSource.PlayOneShot(SoundUtils.GetAudioClip("BDArmory/Sounds/popThrust")); rcsTransforms[i].emit = true; rcsFiredTimes[i] = Time.time; } @@ -1979,17 +2647,17 @@ void DoRCS() { Debug.LogError("[BDArmory.MissileLauncher]: DEBUG " + e.Message); - try { Debug.LogError("[BDArmory.MissileLauncher]: DEBUG null part?: " + (part == null)); } catch (Exception e2) { Debug.LogError("[BDArmory.MissileLauncher]: DEBUG part: " + e2.Message); } - try { Debug.LogError("[BDArmory.MissileLauncher]: DEBUG null part.rb?: " + (part.rb == null)); } catch (Exception e2) { Debug.LogError("[BDArmory.MissileLauncher]: DEBUG part.rb: " + e2.Message); } - try { Debug.LogError("[BDArmory.MissileLauncher]: DEBUG null vessel?: " + (vessel == null)); } catch (Exception e2) { Debug.LogError("[BDArmory.MissileLauncher]: DEBUG vessel: " + e2.Message); } - try { Debug.LogError("[BDArmory.MissileLauncher]: DEBUG null sfAudioSource?: " + (sfAudioSource == null)); } catch (Exception e2) { Debug.LogError("[BDArmory.MissileLauncher]: sfAudioSource: " + e2.Message); } - try { Debug.LogError("[BDArmory.MissileLauncher]: DEBUG null rcsTransforms?: " + (rcsTransforms == null)); } catch (Exception e2) { Debug.LogError("[BDArmory.MissileLauncher]: DEBUG rcsTransforms: " + e2.Message); } + try { Debug.LogWarning("[BDArmory.MissileLauncher]: DEBUG null part?: " + (part == null)); } catch (Exception e2) { Debug.LogWarning("[BDArmory.MissileLauncher]: DEBUG part: " + e2.Message); } + try { Debug.LogWarning("[BDArmory.MissileLauncher]: DEBUG null part.rb?: " + (part.rb == null)); } catch (Exception e2) { Debug.LogWarning("[BDArmory.MissileLauncher]: DEBUG part.rb: " + e2.Message); } + try { Debug.LogWarning("[BDArmory.MissileLauncher]: DEBUG null vessel?: " + (vessel == null)); } catch (Exception e2) { Debug.LogWarning("[BDArmory.MissileLauncher]: DEBUG vessel: " + e2.Message); } + try { Debug.LogWarning("[BDArmory.MissileLauncher]: DEBUG null sfAudioSource?: " + (sfAudioSource == null)); } catch (Exception e2) { Debug.LogWarning("[BDArmory.MissileLauncher]: sfAudioSource: " + e2.Message); } + try { Debug.LogWarning("[BDArmory.MissileLauncher]: DEBUG null rcsTransforms?: " + (rcsTransforms == null)); } catch (Exception e2) { Debug.LogWarning("[BDArmory.MissileLauncher]: DEBUG rcsTransforms: " + e2.Message); } if (rcsTransforms != null) { for (int i = 0; i < 4; ++i) - try { Debug.LogError("[BDArmory.MissileLauncher]: DEBUG null rcsTransforms[" + i + "]?: " + (rcsTransforms[i] == null)); } catch (Exception e2) { Debug.LogError("[BDArmory.MissileLauncher]: DEBUG rcsTransforms[" + i + "]: " + e2.Message); } + try { Debug.LogWarning("[BDArmory.MissileLauncher]: DEBUG null rcsTransforms[" + i + "]?: " + (rcsTransforms[i] == null)); } catch (Exception e2) { Debug.LogWarning("[BDArmory.MissileLauncher]: DEBUG rcsTransforms[" + i + "]: " + e2.Message); } } - try { Debug.LogError("[BDArmory.MissileLauncher]: DEBUG null rcsFiredTimes?: " + (rcsFiredTimes == null)); } catch (Exception e2) { Debug.LogError("[BDArmory.MissileLauncher]: DEBUG rcsFiredTimes: " + e2.Message); } + try { Debug.LogWarning("[BDArmory.MissileLauncher]: DEBUG null rcsFiredTimes?: " + (rcsFiredTimes == null)); } catch (Exception e2) { Debug.LogWarning("[BDArmory.MissileLauncher]: DEBUG rcsFiredTimes: " + e2.Message); } throw; // Re-throw the exception so behaviour is unchanged so we see it. } } @@ -2002,8 +2670,9 @@ public void KillRCS() if (rightRCS) rightRCS.emit = false; } - void OnGUI() + protected override void OnGUI() { + base.OnGUI(); if (HighLogic.LoadedSceneIsFlight) { try @@ -2055,7 +2724,7 @@ void SimpleDrag() void ParseAntiRadTargetTypes() { - antiradTargets = Utils.ParseToFloatArray(antiradTargetTypes); + antiradTargets = OtherUtils.ParseToFloatArray(antiradTargetTypes); } void ParseModes() @@ -2074,7 +2743,12 @@ void ParseModes() case "aampure": GuidanceMode = GuidanceModes.AAMPure; break; - + case "aamloft": + GuidanceMode = GuidanceModes.AAMLoft; + break; + /*case "aamhybrid": + GuidanceMode = GuidanceModes.AAMHybrid; + break;*/ case "agm": GuidanceMode = GuidanceModes.AGM; break; @@ -2103,6 +2777,14 @@ void ParseModes() GuidanceMode = GuidanceModes.SLW; break; + case "pronav": + GuidanceMode = GuidanceModes.PN; + break; + + case "augpronav": + GuidanceMode = GuidanceModes.APN; + break; + default: GuidanceMode = GuidanceModes.None; break; @@ -2165,6 +2847,80 @@ void ParseModes() TargetingModeTerminal = TargetingModes.None; break; } + + terminalHomingType = terminalHomingType.ToLower(); + switch (terminalHomingType) + { + case "aam": + homingModeTerminal = GuidanceModes.AAMLead; + break; + + case "aamlead": + homingModeTerminal = GuidanceModes.AAMLead; + break; + + case "aampure": + homingModeTerminal = GuidanceModes.AAMPure; + break; + case "aamloft": + homingModeTerminal = GuidanceModes.AAMLoft; + break; + case "agm": + homingModeTerminal = GuidanceModes.AGM; + break; + + case "agmballistic": + homingModeTerminal = GuidanceModes.AGMBallistic; + break; + + case "cruise": + homingModeTerminal = GuidanceModes.Cruise; + break; + + case "sts": + homingModeTerminal = GuidanceModes.STS; + break; + + case "rcs": + homingModeTerminal = GuidanceModes.RCS; + break; + + case "beamriding": + homingModeTerminal = GuidanceModes.BeamRiding; + break; + + case "slw": + homingModeTerminal = GuidanceModes.SLW; + break; + + case "pronav": + homingModeTerminal = GuidanceModes.PN; + break; + + case "augpronav": + homingModeTerminal = GuidanceModes.APN; + break; + + default: + homingModeTerminal = GuidanceModes.None; + break; + } + + if (!terminalHoming && GuidanceMode == GuidanceModes.AAMLoft) + { + if (homingModeTerminal == GuidanceModes.None) + { + homingModeTerminal = GuidanceModes.PN; + Debug.Log($"[BDArmory.MissileLauncher]: Error in configuration of {part.name}, homingType is AAMLoft but no terminal guidance mode was specified, defaulting to pro-nav."); + } + else if (!(homingModeTerminal == GuidanceModes.AAMLead || homingModeTerminal == GuidanceModes.AAMPure || homingModeTerminal == GuidanceModes.PN || homingModeTerminal == GuidanceModes.APN)) + { + terminalHoming = true; + Debug.LogWarning($"[BDArmory.MissileLauncher]: Error in configuration of {part.name}, homingType is AAMLoft but an unsupported terminalHomingType: {terminalHomingType} was used without setting terminalHoming = true. "); + } + } + + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MissileLauncher]: parsing guidance and homing complete on {GetPartName()}"); } private string GetBrevityCode() @@ -2224,6 +2980,10 @@ private string GetBrevityCode() return TargetingModeTerminal != TargetingModes.None ? "GPS/Terminal" : "GPS"; } + if (TargetingMode == TargetingModes.None) + { + return TargetingModeTerminal != TargetingModes.None ? "Inertial/Terminal" : "Unguided"; + } // default: return "Unguided"; } @@ -2245,6 +3005,12 @@ public override string GetInfo() output.AppendLine($"Min Range: {minStaticLaunchRange} m"); output.AppendLine($"Max Range: {maxStaticLaunchRange} m"); + if (useFuel && weaponClass == WeaponClasses.Missile) + { + double dV = Math.Round(GetDeltaV(), 1); + if (dV > 0) output.AppendLine($"Total DeltaV: {dV} m/s"); + } + if (TargetingMode == TargetingModes.Radar) { if (activeRadarRange > 0) @@ -2255,6 +3021,7 @@ public override string GetInfo() else output.AppendLine($"- Lock/Track: {RadarUtils.MISSILE_DEFAULT_LOCKABLE_RCS} m^2 @ {activeRadarRange / 1000} km"); output.AppendLine($"- LOAL: {radarLOAL}"); + if (radarLOAL) output.AppendLine($" - Max Radar Search Time: {radarTimeout}"); } output.AppendLine($"Max Offborsight: {maxOffBoresight}"); output.AppendLine($"Locked FOV: {lockedSensorFOV}"); @@ -2262,9 +3029,9 @@ public override string GetInfo() if (TargetingMode == TargetingModes.Heat) { - output.AppendLine($"All Aspect: {allAspect}"); + output.AppendLine($"Uncaged Lock: {uncagedLock}"); output.AppendLine($"Min Heat threshold: {heatThreshold}"); - output.AppendLine($"Max Offborsight: {maxOffBoresight}"); + output.AppendLine($"Max Offboresight: {maxOffBoresight}"); output.AppendLine($"Locked FOV: {lockedSensorFOV}"); } @@ -2283,13 +3050,44 @@ public override string GetInfo() else output.AppendLine($"- Lock/Track: {RadarUtils.MISSILE_DEFAULT_LOCKABLE_RCS} m^2 @ {activeRadarRange / 1000} km"); output.AppendLine($"- LOAL: {radarLOAL}"); + if (radarLOAL) output.AppendLine($" - Radar Search Time: {radarTimeout}"); output.AppendLine($"Max Offborsight: {maxOffBoresight}"); output.AppendLine($"Locked FOV: {lockedSensorFOV}"); } if (TargetingModeTerminal == TargetingModes.Heat) { - output.AppendLine($"All Aspect: {allAspect}"); + output.AppendLine($"Uncaged Lock: {uncagedLock}"); + output.AppendLine($"Min Heat threshold: {heatThreshold}"); + output.AppendLine($"Max Offborsight: {maxOffBoresight}"); + output.AppendLine($"Locked FOV: {lockedSensorFOV}"); + } + } + } + + if (TargetingMode == TargetingModes.None) + { + output.AppendLine($"Terminal Maneuvering: {terminalManeuvering}"); + if (terminalGuidanceType != "") + { + output.AppendLine($"Terminal guidance: {terminalGuidanceType} @ distance: {terminalGuidanceDistance} m"); + + if (TargetingModeTerminal == TargetingModes.Radar) + { + output.AppendLine($"Active Radar Range: {activeRadarRange} m"); + if (activeRadarLockTrackCurve.maxTime > 0) + output.AppendLine($"- Lock/Track: {activeRadarLockTrackCurve.Evaluate(activeRadarLockTrackCurve.maxTime)} m^2 @ {activeRadarLockTrackCurve.maxTime} km"); + else + output.AppendLine($"- Lock/Track: {RadarUtils.MISSILE_DEFAULT_LOCKABLE_RCS} m^2 @ {activeRadarRange / 1000} km"); + output.AppendLine($"- LOAL: {radarLOAL}"); + if (radarLOAL) output.AppendLine($" - Radar Search Time: {radarTimeout}"); + output.AppendLine($"Max Offborsight: {maxOffBoresight}"); + output.AppendLine($"Locked FOV: {lockedSensorFOV}"); + } + + if (TargetingModeTerminal == TargetingModes.Heat) + { + output.AppendLine($"Uncaged Lock: {uncagedLock}"); output.AppendLine($"Min Heat threshold: {heatThreshold}"); output.AppendLine($"Max Offborsight: {maxOffBoresight}"); output.AppendLine($"Locked FOV: {lockedSensorFOV}"); @@ -2302,6 +3100,18 @@ public override string GetInfo() while (partModules.MoveNext()) { if (partModules.Current == null) continue; + if (partModules.Current.moduleName == "MultiMissileLauncher") + { + if (((MultiMissileLauncher)partModules.Current).isClusterMissile) + { + output.AppendLine($"Cluster Missile:"); + output.AppendLine($"- SubMunition Count: {((MultiMissileLauncher)partModules.Current).salvoSize} "); + float tntMass = ((MultiMissileLauncher)partModules.Current).tntMass; + output.AppendLine($"- Blast radius: {Math.Round(BlastPhysicsUtils.CalculateBlastRange(tntMass), 2)} m"); + output.AppendLine($"- tnt Mass: {tntMass} kg"); + } + if (((MultiMissileLauncher)partModules.Current).isMultiLauncher) continue; + } if (partModules.Current.moduleName == "BDExplosivePart") { ((BDExplosivePart)partModules.Current).ParseWarheadType(); @@ -2314,18 +3124,22 @@ public override string GetInfo() output.AppendLine($"- Blast radius: {Math.Round(BlastPhysicsUtils.CalculateBlastRange(tntMass), 2)} m"); output.AppendLine($"- tnt Mass: {tntMass} kg"); output.AppendLine($"- {((BDExplosivePart)partModules.Current).warheadReportingName} warhead"); + if (((BDExplosivePart)partModules.Current).warheadType == "shapedcharge") + output.AppendLine($"- Penetration: {ProjectileUtils.CalculatePenetration(((BDExplosivePart)partModules.Current).caliber > 0 ? ((BDExplosivePart)partModules.Current).caliber * 0.05f : 6f * 0.05f, 5000f, ((BDExplosivePart)partModules.Current).tntMass * 0.0555f, ((BDExplosivePart)partModules.Current).apMod):F2} mm"); } - else if (partModules.Current.moduleName == "ModuleEMP") + if (partModules.Current.moduleName == "ModuleEMP") { float proximity = ((ModuleEMP)partModules.Current).proximity; output.AppendLine($"- EMP Blast Radius: {proximity} m"); } - else if (partModules.Current.moduleName == "BDModuleNuke") + if (partModules.Current.moduleName == "BDModuleNuke") { float yield = ((BDModuleNuke)partModules.Current).yield; float radius = ((BDModuleNuke)partModules.Current).thermalRadius; + float EMPRadius = ((BDModuleNuke)partModules.Current).isEMP ? BDAMath.Sqrt(yield) * 500 : -1; output.AppendLine($"- Yield: {yield} kT"); output.AppendLine($"- Max radius: {radius} m"); + if (EMPRadius > 0) output.AppendLine($"- EMP Blast Radius: {EMPRadius} m"); } else continue; break; @@ -2356,7 +3170,7 @@ static void AttachExhaustPrefab(string prefabPath, MissileLauncher missileLaunch missileLauncher.exhaustPrefabs.Add(exhaustPrefab); missileLauncher.part.OnJustAboutToDie += missileLauncher.DetachExhaustPrefabs; missileLauncher.part.OnJustAboutToBeDestroyed += missileLauncher.DetachExhaustPrefabs; - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.MissileLauncher]: Exhaust prefab " + exhaustPrefab.name + " added to " + missileLauncher.shortName + " on " + (missileLauncher.vessel != null ? missileLauncher.vessel.vesselName : "unknown")); + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.MissileLauncher]: Exhaust prefab " + exhaustPrefab.name + " added to " + missileLauncher.shortName + " on " + (missileLauncher.vessel != null ? missileLauncher.vessel.vesselName : "unknown")); } static void CreateExhaustPool(string prefabPath) @@ -2383,10 +3197,28 @@ void DetachExhaustPrefabs() if (exhaustPrefab == null) continue; exhaustPrefab.transform.parent = null; exhaustPrefab.SetActive(false); - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.MissileLauncher]: Exhaust prefab " + exhaustPrefab.name + " removed from " + shortName + " on " + (vessel != null ? vessel.vesselName : "unknown")); + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.MissileLauncher]: Exhaust prefab " + exhaustPrefab.name + " removed from " + shortName + " on " + (vessel != null ? vessel.vesselName : "unknown")); } exhaustPrefabs.Clear(); } #endregion + public double GetDeltaV() + { + double specificImpulse; + double deltaV; + double massFlowRate; + + massFlowRate = (boostTime == 0) ? 0 : boosterFuelMass / boostTime; + specificImpulse = (massFlowRate == 0) ? 0 : thrust / (massFlowRate * 9.81); + deltaV = specificImpulse * 9.81 * Math.Log(part.mass / (part.mass - boosterFuelMass)); + + double mass = part.mass; + massFlowRate = (cruiseTime == 0) ? 0 : cruiseFuelMass / cruiseTime; + if (boosterFuelMass > 0) mass -= boosterFuelMass; + specificImpulse = (massFlowRate == 0) ? 0 : cruiseThrust / (massFlowRate * 9.81); + deltaV += (specificImpulse * 9.81 * Math.Log(mass / (mass - cruiseFuelMass))); + + return deltaV; + } } } diff --git a/BDArmory/Weapons/Missiles/ModuleMissileRearm.cs b/BDArmory/Weapons/Missiles/ModuleMissileRearm.cs new file mode 100644 index 000000000..2c7be2d75 --- /dev/null +++ b/BDArmory/Weapons/Missiles/ModuleMissileRearm.cs @@ -0,0 +1,273 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using KSP.UI.Screens; +using UnityEngine; + +using BDArmory.WeaponMounts; +using BDArmory.Settings; +using System.Text; + +namespace BDArmory.Weapons.Missiles +{ + public class ModuleMissileRearm : PartModule, IPartMassModifier, IPartCostModifier + { + public float GetModuleMass(float baseMass, ModifierStagingSituation situation) => Mathf.Max((isMultiLauncher ? ammoCount : ammoCount - 1), 0) * missileMass; + + public ModifierChangeWhen GetModuleMassChangeWhen() => ModifierChangeWhen.FIXED; + public float GetModuleCost(float baseCost, ModifierStagingSituation situation) => Mathf.Max((isMultiLauncher ? ammoCount : ammoCount - 1), 0) * missileCost; + public ModifierChangeWhen GetModuleCostChangeWhen() => ModifierChangeWhen.FIXED; + + private float missileMass = 0; + private float missileCost = 0; + + public bool isMultiLauncher = false; + + [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = true, guiName = "#LOC_BDArmory_OrdinanceAvailable"),//Ordinance Available +UI_FloatRange(minValue = 1f, maxValue = 4, stepIncrement = 1f, scene = UI_Scene.Editor)] + public float ammoCount = 1; //need to figure out where ammo is stored, for mass addition/subtraction - in the missile? external missile ammo bin? CoM? + + [KSPField(isPersistant = true)] + public string MissileName = "bahaAim120"; + + [KSPField] public float reloadTime = 5f; + [KSPField] public bool AccountForAmmo = true; + [KSPField] public float maxAmmo = 20; + public float tntmass = 1; + AvailablePart missilePart; + public Part SpawnedMissile; + public void SpawnMissile(Transform MissileTransform, float offset = 0, bool deductAmmo = true) + { + if (ammoCount >= 1 || BDArmorySettings.INFINITE_ORDINANCE) + { + if (missilePart != null) + { + if (MissileTransform == null) MissileTransform = part.partTransform; + + foreach (PartModule m in missilePart.partPrefab.Modules) + { + if (m.moduleName == "MissileLauncher") + { + var partNode = new ConfigNode(); + PartSnapshot(missilePart.partPrefab).CopyTo(partNode); + //SpawnedMissile = CreatePart(partNode, MissileTransform.transform.position - MissileTransform.TransformDirection(missilePart.partPrefab.srfAttachNode.originalPosition), + SpawnedMissile = CreatePart(partNode, offset > 0 ? (MissileTransform.position + MissileTransform.forward * offset) : MissileTransform.transform.position, MissileTransform.rotation, this.part); + ModuleMissileRearm MMR = SpawnedMissile.FindModuleImplementing(); + if (MMR != null) SpawnedMissile.RemoveModule(MMR); + if (!BDArmorySettings.INFINITE_ORDINANCE && deductAmmo) ammoCount--; + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.ModuleMissileRearm] spawned " + SpawnedMissile.name + "; ammo remaining: " + ammoCount); + return; + } + } + } + } + } + + public override void OnStart(PartModule.StartState state) + { + this.enabled = true; + this.part.force_activate(); + var MML = part.FindModuleImplementing(); + if (MML == null || MML && MML.isClusterMissile) MissileName = part.name; + StartCoroutine(GetMissileValues()); + //GameEvents.onEditorShipModified.Add(ShipModified); + UI_FloatRange Ammo = (UI_FloatRange)Fields["ammoCount"].uiControlEditor; + Ammo.maxValue = maxAmmo; + } + private void OnDestroy() + { + GameEvents.onEditorShipModified.Remove(ShipModified); + } + + public void ShipModified(ShipConstruct data) + { + if (part.parent) + { + + if (part.parent.FindModuleImplementing()) //turrets work... sorta. Missiles are reloading where they should be, but there's some massive force being imparted on the turret every launch + {// test UpdateMissileChildren fix used for rotary rails? + ammoCount = 1; + Fields["ammoCount"].guiActiveEditor = false; + } + else + { + Fields["ammoCount"].guiActiveEditor = true; + } + + } + } + + public void UpdateMissileValues() + { + StartCoroutine(GetMissileValues()); + } + IEnumerator GetMissileValues() + { + yield return new WaitForFixedUpdate(); + MissileLauncher ml = part.FindModuleImplementing(); + ml.reloadableRail = this; + using (var parts = PartLoader.LoadedPartsList.GetEnumerator()) + while (parts.MoveNext()) + { + if (parts.Current.partConfig == null || parts.Current.partPrefab == null) + continue; + if (parts.Current.partPrefab.partInfo.name != MissileName) continue; + missilePart = parts.Current; + //Debug.Log($"[BDArmory.ModuleMissileRearm]: found {missilePart.partPrefab.partInfo.name}"); + break; + } + if (missilePart == null) + { + Debug.LogWarning($"[BDArmory.ModuleMissileRearm]: Failed to find missile part on {part.partInfo.name}"); + tntmass = 0; + missileCost = 0; + missileMass = 0; + yield break; + } + var explosivePart = missilePart.partPrefab.FindModuleImplementing(); + if (explosivePart != null) tntmass = explosivePart.tntMass; + else tntmass = 0; + if (AccountForAmmo) + { + missileCost = missilePart.partPrefab.partInfo.cost; + missileMass = missilePart.partPrefab.mass; + } + else + { + missileCost = 0; + missileMass = 0; + } + } + + static IEnumerator FinalizeMissile(Part missile, Part launcher) + { + //Debug.Log("[BDArmory.ModuleMissileRearm]: Creating " + missile); + string originatingVesselName = missile.vessel.vesselName; + missile.physicalSignificance = Part.PhysicalSignificance.NONE; + missile.PromoteToPhysicalPart(); + var childColliders = missile.GetComponentsInChildren(includeInactive: false); + CollisionManager.IgnoreCollidersOnVessel(launcher.vessel, childColliders); + missile.Unpack(); + missile.InitializeModules(); + Vessel newVessel = missile.gameObject.AddComponent(); + newVessel.id = Guid.NewGuid(); + if (newVessel.Initialize(false)) + { + newVessel.vesselName = Vessel.AutoRename(newVessel, originatingVesselName); + newVessel.IgnoreGForces(10); + newVessel.currentStage = StageManager.RecalculateVesselStaging(newVessel); + missile.setParent(null); + } + yield return new WaitWhile(() => !missile.started && missile.State != PartStates.DEAD); + if (missile.State == PartStates.DEAD) + { + Debug.Log("[BDArmory.ModuleMissileRearm]: Error; " + missile + " died before being fully initialized"); + yield break; + } + } + + public static ConfigNode PartSnapshot(Part part) + { + var node = new ConfigNode("PART"); + var snapshot = new ProtoPartSnapshot(part, null); + + snapshot.attachNodes = new List(); + snapshot.srfAttachNode = new AttachNodeSnapshot("attach,-1"); + snapshot.symLinks = new List(); + snapshot.symLinkIdxs = new List(); + snapshot.Save(node); + + // Prune unimportant data + node.RemoveValues("parent"); + node.RemoveValues("position"); + node.RemoveValues("rotation"); + node.RemoveValues("istg"); + node.RemoveValues("dstg"); + node.RemoveValues("sqor"); + node.RemoveValues("sidx"); + node.RemoveValues("attm"); + node.RemoveValues("srfN"); + node.RemoveValues("attN"); + node.RemoveValues("connected"); + node.RemoveValues("attached"); + node.RemoveValues("flag"); + node.RemoveNodes("ACTIONS"); + + var module_nodes = node.GetNodes("MODULE"); + var prefab_modules = part.partInfo.partPrefab.GetComponents(); + node.RemoveNodes("MODULE"); + + for (int i = 0; i < prefab_modules.Length && i < module_nodes.Length; i++) + { + var module = module_nodes[i]; + var name = module.GetValue("name") ?? ""; + + node.AddNode(module); + module.RemoveNodes("ACTIONS"); + } + return node; + } + + public delegate void OnPartReady(Part affectedPart); + + /// Creates a new part from the config. + /// Config to read part from. + /// Initial position of the new part. + /// Initial rotation of the new part. + /// + + public static Part CreatePart( + ConfigNode partConfig, + Vector3 position, + Quaternion rotation, + Part launcherPart) + { + var refVessel = launcherPart.vessel; + var partNodeCopy = new ConfigNode(); + partConfig.CopyTo(partNodeCopy); + var snapshot = + new ProtoPartSnapshot(partNodeCopy, refVessel.protoVessel, HighLogic.CurrentGame); + if (HighLogic.CurrentGame.flightState.ContainsFlightID(snapshot.flightID) + || snapshot.flightID == 0) + { + snapshot.flightID = ShipConstruction.GetUniqueFlightID(HighLogic.CurrentGame.flightState); + } + snapshot.parentIdx = 0; + snapshot.position = position; + snapshot.rotation = rotation; + snapshot.stageIndex = 0; + snapshot.defaultInverseStage = 0; + snapshot.seqOverride = -1; + snapshot.inStageIndex = -1; + snapshot.attachMode = (int)AttachModes.SRF_ATTACH; + snapshot.attached = false; + + var newPart = snapshot.Load(refVessel, false); + newPart.transform.position = position; + newPart.transform.rotation = rotation; + if (newPart.rb != null) + { + newPart.rb.velocity = launcherPart.Rigidbody.velocity; + newPart.rb.angularVelocity = launcherPart.Rigidbody.angularVelocity; + } + newPart.missionID = launcherPart.missionID; + newPart.UpdateOrgPosAndRot(newPart.vessel.rootPart); + + newPart.StartCoroutine(FinalizeMissile(newPart, launcherPart)); + return newPart; + } + public override string GetInfo() + { + StringBuilder output = new StringBuilder(); + + output.Append(Environment.NewLine); + output.AppendLine($"Missile Rearming"); + output.AppendLine($"- Reload Time: {reloadTime} s"); + output.AppendLine($"- Maximum Ordinance: {maxAmmo}"); + output.AppendLine($"- Ammo Mass/Cost: {AccountForAmmo}"); + return output.ToString(); + } + } +} \ No newline at end of file diff --git a/BDArmory/Weapons/Missiles/MultiMissileLauncher.cs b/BDArmory/Weapons/Missiles/MultiMissileLauncher.cs new file mode 100644 index 000000000..0fd408426 --- /dev/null +++ b/BDArmory/Weapons/Missiles/MultiMissileLauncher.cs @@ -0,0 +1,886 @@ +using BDArmory.Competition; +using BDArmory.Control; +using BDArmory.Radar; +using BDArmory.Settings; +using BDArmory.Targeting; +using BDArmory.UI; +using BDArmory.Utils; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; +using static BDArmory.Weapons.Missiles.MissileBase; + +namespace BDArmory.Weapons.Missiles +{ + /// + /// Add-on Module to MissileLauncher to extend Launcher functionality to include cluster missiles and multi-missile pods + /// + + public class MultiMissileLauncher : PartModule + { + public static Dictionary mslDummyPool = new Dictionary(); + [KSPField(isPersistant = true)] + Vector3 dummyScale = Vector3.one; + Coroutine missileSalvo; + + [KSPField(isPersistant = true, guiActive = false, guiName = "#LOC_BDArmory_WeaponName", guiActiveEditor = false), UI_Label(affectSymCounterparts = UI_Scene.All, scene = UI_Scene.All)]//Weapon Name + public string loadedMissileName; + + Transform[] launchTransforms; + [KSPField(isPersistant = true)] public string subMunitionName; //name of missile in .cfg - e.g. "bahaAim120" + [KSPField(isPersistant = true)] public string subMunitionPath; //model path for missile + [KSPField] public string launchTransformName; //name of transform launcTransforms are parented to - see Rocketlauncher transform hierarchy + [KSPField] public int salvoSize = 1; //leave blank to have salvoSize = launchTransforms.count + [KSPField] public bool isClusterMissile = false; //cluster submunitions deployed instead of standard detonation? Fold this into warHeadType? + [KSPField] public bool isMultiLauncher = false; //is this a pod or launcher holding multiple missiles that fire in a salvo? + [KSPField] public bool useSymCounterpart = false; //have symmetrically placed parts fire along with this part as part of salvo? Requires isMultMissileLauncher = true; + [KSPField] public bool overrideReferenceTransform = false; //override the missileReferenceTransform in Missilelauncher to use vessel prograde + [KSPField] public float rippleRPM = 650; + [KSPField] public float launcherCooldown = 0; //additional delay after firing before launcher can fire next salvo + [KSPField] public float offset = 0; //add an offset to missile spawn position? + [KSPField] public string deployAnimationName; + [KSPField] public string RailNode = "rail"; //name of attachnode for VLS MMLs to set missile loadout + [KSPField] public float tntMass = 1; //for MissileLauncher GetInfo() + [KSPField] public bool OverrideDropSettings = false; //for MissileLauncher GetInfo() + [KSPField] public bool displayOrdinance = true; //display missile dummies (for rails and the like) or hide them (bomblet dispensers, gun-launched missiles, etc) + [KSPField] public bool permitJettison = false; //allow jettisoning of missiles for multimissile launchrails and similar + AnimationState deployState; + ModuleMissileRearm missileSpawner = null; + MissileLauncher missileLauncher = null; + MissileFire wpm = null; + private int tubesFired = 0; + [KSPField(isPersistant = true)] + private bool LoadoutModified = false; + public BDTeam Team = BDTeam.Get("Neutral"); + public void Start() + { + MakeMissileArray(); + GameEvents.onEditorShipModified.Add(ShipModified); + if (HighLogic.LoadedSceneIsFlight) + { + GameEvents.onPartDie.Add(OnPartDie); + } + if (!string.IsNullOrEmpty(deployAnimationName)) + { + deployState = GUIUtils.SetUpSingleAnimation(deployAnimationName, part); + if (deployState != null) + { + deployState.normalizedTime = 0; + deployState.speed = 0; + deployState.enabled = true; + } + } + StartCoroutine(DelayedStart()); + } + + IEnumerator DelayedStart() + { + yield return new WaitForFixedUpdate(); + missileLauncher = part.FindModuleImplementing(); + missileSpawner = part.FindModuleImplementing(); + if (missileSpawner == null) //MultiMissile launchers/cluster missiles need a MMR module for spawning their submunitions, so add one if not present in case cfg not set up properly + { + missileSpawner = (ModuleMissileRearm)part.AddModule("ModuleMissileRearm"); + missileSpawner.maxAmmo = isClusterMissile ? salvoSize : salvoSize * 5; + missileSpawner.ammoCount = launchTransforms.Length; + missileSpawner.MissileName = subMunitionName; + if (!isClusterMissile) //Clustermissiles replace/generate MMR on launch, other missiles should have it in the .cfg + Debug.LogError($"[BDArmory.MultiMissileLauncher] no ModuleMissileRearm on {part.name}. Please fix your .cfg"); + } + missileSpawner.isMultiLauncher = isMultiLauncher; + if (missileLauncher != null) //deal with race condition/'MissileLauncher' loading before 'MultiMissileLauncher' and 'ModuleMissilerearm' by moving all relevant flags and values to a single location + { + missileLauncher.reloadableRail = missileSpawner; + missileLauncher.hasAmmo = true; + missileLauncher.multiLauncher = this; + + if (isClusterMissile) + { + missileSpawner.MissileName = missileLauncher.missileName; + missileLauncher.DetonationDistance = 750; + missileLauncher.blastRadius = 750; //clustermissile det radius hardcoded for now + missileLauncher.Fields["DetonationDistance"].guiActive = false; + missileLauncher.Fields["DetonationDistance"].guiActiveEditor = false; + missileLauncher.DetonateAtMinimumDistance = false; + missileLauncher.Fields["DetonateAtMinimumDistance"].guiActive = true; + missileLauncher.Fields["DetonateAtMinimumDistance"].guiActiveEditor = true; + if (missileSpawner.maxAmmo == 1) + { + missileSpawner.Fields["ammoCount"].guiActive = false; + missileSpawner.Fields["ammoCount"].guiActiveEditor = false; + } + } + if (isMultiLauncher) + { + if (string.IsNullOrEmpty(subMunitionName)) + { + Fields["loadedMissileName"].guiActive = true; + Fields["loadedMissileName"].guiActiveEditor = true; + } + missileLauncher.missileName = subMunitionName; + if (!permitJettison) missileLauncher.Events["Jettison"].guiActive = false; + if (OverrideDropSettings) + { + missileLauncher.Fields["dropTime"].guiActive = false; + missileLauncher.Fields["dropTime"].guiActiveEditor = false; + missileLauncher.dropTime = 0; + missileLauncher.Fields["decoupleSpeed"].guiActive = false; + missileLauncher.Fields["decoupleSpeed"].guiActiveEditor = false; + missileLauncher.decoupleSpeed = 10; + missileLauncher.Events["decoupleForward"].guiActive = false; + missileLauncher.Events["decoupleForward"].guiActiveEditor = false; + missileLauncher.decoupleForward = true; + } + float bRadius = 0; + using (var parts = PartLoader.LoadedPartsList.GetEnumerator()) + while (parts.MoveNext()) + { + if (parts.Current.partConfig == null || parts.Current.partPrefab == null) continue; + if (parts.Current.partPrefab.partInfo.name != subMunitionName) continue; + var explosivePart = parts.Current.partPrefab.FindModuleImplementing(); + bRadius = explosivePart != null ? explosivePart.GetBlastRadius() : 0; + } + if (bRadius == 0) + { + Debug.Log("[multiMissileLauncher.GetBlastRadius] needing to use MMR tntmass value!"); + bRadius = BlastPhysicsUtils.CalculateBlastRange(missileSpawner.tntmass); + } + missileLauncher.blastRadius = bRadius; + + if (missileLauncher.GuidanceMode == GuidanceModes.AAMLead || missileLauncher.GuidanceMode == GuidanceModes.AAMPure || missileLauncher.GuidanceMode == GuidanceModes.PN || missileLauncher.GuidanceMode == GuidanceModes.APN) + { + missileLauncher.DetonationDistance = bRadius * 0.25f; + } + else + { + //DetonationDistance = GetBlastRadius() * 0.05f; + missileLauncher.DetonationDistance = 0f; + } + } + GUIUtils.RefreshAssociatedWindows(part); + } + missileSpawner.UpdateMissileValues(); + if (LoadoutModified) + { + using (var parts = PartLoader.LoadedPartsList.GetEnumerator()) + while (parts.MoveNext()) + { + if (parts.Current.partConfig == null || parts.Current.partPrefab == null) + continue; + if (parts.Current.partPrefab.partInfo.name != subMunitionName) continue; + UpdateFields(parts.Current.partPrefab.FindModuleImplementing(), false); + break; + } + } + } + private void OnDestroy() + { + GameEvents.onEditorShipModified.Remove(ShipModified); + GameEvents.onPartDie.Remove(OnPartDie); + } + + + void OnPartDie() { OnPartDie(part); } + + void OnPartDie(Part p) + { + if (p == part) + { + foreach (var existingDummy in part.GetComponents()) + { + existingDummy.Deactivate(); + } + } + } + + public void ShipModified(ShipConstruct data) + { + if (part.children.Count > 0) + { + using (List.Enumerator stackNode = part.attachNodes.GetEnumerator()) + while (stackNode.MoveNext()) + { + if (stackNode.Current?.nodeType != AttachNode.NodeType.Stack) continue; + if (stackNode.Current.id != RailNode) continue; + { + if (stackNode.Current.attachedPart is Part missile) + { + if (missile == null) return; + + if (missile.FindModuleImplementing()) + { + subMunitionName = missile.name; + subMunitionPath = GetMeshurl((UrlDir.UrlConfig)GameDatabase.Instance.root.GetConfig(missile.partInfo.partUrl)); + PopulateMissileDummies(true); + MissileLauncher MLConfig = missile.FindModuleImplementing(); + LoadoutModified = true; + if (missileSpawner) + { + missileSpawner.MissileName = subMunitionName; + missileSpawner.UpdateMissileValues(); + } + UpdateFields(MLConfig, true); + EditorLogic.DeletePart(missile); + } + } + } + } + } + } + + private string GetMeshurl(UrlDir.UrlConfig cfgdir) + { + //check if part uses a MODEL node to grab an (external?) .mu file + string url; + if (cfgdir.config.HasNode("MODEL")) + { + var MODEL = cfgdir.config.GetNode("MODEL"); + url = MODEL.GetValue("model") ?? ""; + dummyScale = Vector3.one; + if (MODEL.HasValue("scale")) + { + string[] strings = MODEL.GetValue("scale").Split(","[0]); + dummyScale.x = Single.Parse(strings[0]); + dummyScale.y = Single.Parse(strings[1]); + dummyScale.z = Single.Parse(strings[2]); + } + else + { + if (cfgdir.config.HasValue("rescaleFactor")) + { + float scale = Single.Parse(cfgdir.config.GetValue("rescaleFactor")); + dummyScale.x = scale; + dummyScale.y = scale; + dummyScale.z = scale; + } + } + //Debug.Log($"[BDArmory.MultiMissileLauncher] Found model URL of {url} and scale {dummyScale}"); + return url; + + } + string mesh = "model"; + //in case the mesh is not model.mu + if (cfgdir.config.HasValue("mesh")) + { + mesh = cfgdir.config.GetValue("mesh"); + char[] sep = { '.' }; + string[] words = mesh.Split(sep); + mesh = words[0]; + } + if (cfgdir.config.HasValue("rescaleFactor")) + { + float scale = Single.Parse(cfgdir.config.GetValue("rescaleFactor")); + dummyScale.x = scale; + dummyScale.y = scale; + dummyScale.z = scale; + } + url = string.Format("{0}/{1}", cfgdir.parent.parent.url, mesh); + //Debug.Log($"[BDArmory.MultiMissileLauncher] Found model URL of {url} and scale {dummyScale}"); + return url; + } + + void UpdateFields(MissileLauncher MLConfig, bool configurableSettings) + { + missileLauncher.homingType = MLConfig.homingType; //these are all non-persistant, and need to be re-grabbed at launch + missileLauncher.targetingType = MLConfig.targetingType; + missileLauncher.missileType = MLConfig.missileType; + missileLauncher.lockedSensorFOV = MLConfig.lockedSensorFOV; + missileLauncher.lockedSensorFOVBias = MLConfig.lockedSensorFOVBias; + missileLauncher.lockedSensorVelocityBias = MLConfig.lockedSensorVelocityBias; + missileLauncher.heatThreshold = MLConfig.heatThreshold; + missileLauncher.chaffEffectivity = MLConfig.chaffEffectivity; + missileLauncher.allAspect = MLConfig.allAspect; + missileLauncher.uncagedLock = MLConfig.uncagedLock; + missileLauncher.isTimed = MLConfig.isTimed; + missileLauncher.radarLOAL = MLConfig.radarLOAL; + missileLauncher.activeRadarRange = MLConfig.activeRadarRange; + missileLauncher.activeRadarLockTrackCurve = MLConfig.activeRadarLockTrackCurve; + missileLauncher.antiradTargets = MLConfig.antiradTargets; + missileLauncher.steerMult = MLConfig.steerMult; + missileLauncher.thrust = MLConfig.thrust; + missileLauncher.maxAoA = MLConfig.maxAoA; + missileLauncher.optimumAirspeed = MLConfig.optimumAirspeed; + missileLauncher.maxTurnRateDPS = MLConfig.maxTurnRateDPS; + missileLauncher.proxyDetonate = MLConfig.proxyDetonate; + missileLauncher.terminalManeuvering = MLConfig.terminalManeuvering; + missileLauncher.terminalGuidanceType = MLConfig.terminalGuidanceType; + missileLauncher.torpedo = MLConfig.torpedo; + missileLauncher.loftState = 0; + missileLauncher.TimeToImpact = float.PositiveInfinity; + missileLauncher.initMaxAoA = MLConfig.maxAoA; + missileLauncher.homingModeTerminal = MLConfig.homingModeTerminal; + missileLauncher.pronavGain = MLConfig.pronavGain; + missileLauncher.terminalHoming = MLConfig.terminalHoming; + missileLauncher.terminalHomingActive = false; + + if (configurableSettings) + { + missileLauncher.maxStaticLaunchRange = MLConfig.maxStaticLaunchRange; + missileLauncher.minStaticLaunchRange = MLConfig.minStaticLaunchRange; + missileLauncher.engageRangeMin = MLConfig.minStaticLaunchRange; + missileLauncher.engageRangeMax = MLConfig.maxStaticLaunchRange; + if (!overrideReferenceTransform) missileLauncher.maxOffBoresight = MLConfig.maxOffBoresight; //don't overwrite e.g. VLS launcher boresights so they can launch, but still have normal boresight on fired missiles + missileLauncher.DetonateAtMinimumDistance = MLConfig.DetonateAtMinimumDistance; + + missileLauncher.detonationTime = MLConfig.detonationTime; + missileLauncher.DetonationDistance = MLConfig.DetonationDistance; + missileLauncher.BallisticOverShootFactor = MLConfig.BallisticOverShootFactor; + missileLauncher.BallisticAngle = MLConfig.BallisticAngle; + missileLauncher.CruiseAltitude = MLConfig.CruiseAltitude; + missileLauncher.CruiseSpeed = MLConfig.CruiseSpeed; + missileLauncher.CruisePredictionTime = MLConfig.CruisePredictionTime; + if (!OverrideDropSettings) + { + missileLauncher.decoupleForward = MLConfig.decoupleForward; + missileLauncher.dropTime = MLConfig.dropTime; + missileLauncher.decoupleSpeed = MLConfig.decoupleSpeed; + } + else + { + missileLauncher.decoupleForward = true; + missileLauncher.dropTime = 0; + missileLauncher.decoupleSpeed = 10; + } + missileLauncher.clearanceRadius = MLConfig.clearanceRadius; + missileLauncher.clearanceLength = MLConfig.clearanceLength; + missileLauncher.maxAltitude = MLConfig.maxAltitude; + missileLauncher.terminalGuidanceShouldActivate = MLConfig.terminalGuidanceShouldActivate; + missileLauncher.engageAir = MLConfig.engageAir; + missileLauncher.engageGround = MLConfig.engageGround; + missileLauncher.engageMissile = MLConfig.engageMissile; + missileLauncher.engageSLW = MLConfig.engageSLW; + missileLauncher.shortName = MLConfig.shortName; + missileLauncher.blastRadius = -1; + missileLauncher.blastRadius = MLConfig.blastRadius; + missileLauncher.LoftMaxAltitude = MLConfig.LoftMaxAltitude; + missileLauncher.LoftRangeOverride = MLConfig.LoftRangeOverride; + missileLauncher.LoftAltitudeAdvMax = MLConfig.LoftAltitudeAdvMax; + missileLauncher.LoftMinAltitude = MLConfig.LoftMinAltitude; + missileLauncher.LoftAngle = MLConfig.LoftAngle; + missileLauncher.LoftTermAngle = MLConfig.LoftTermAngle; + missileLauncher.LoftRangeFac = MLConfig.LoftRangeFac; + missileLauncher.LoftVelComp = MLConfig.LoftVelComp; + missileLauncher.LoftVertVelComp = MLConfig.LoftVertVelComp; + //missileLauncher.LoftAltComp = LoftAltComp; + missileLauncher.terminalHomingRange = MLConfig.terminalHomingRange; + } + missileLauncher.GetBlastRadius(); + GUIUtils.RefreshAssociatedWindows(missileLauncher.part); + missileLauncher.SetFields(); + missileLauncher.Sublabel = $"Guidance: {Enum.GetName(typeof(TargetingModes), missileLauncher.TargetingMode)}; Max Range: {Mathf.Round(missileLauncher.engageRangeMax / 100) / 10} km; Remaining: {missileLauncher.missilecount}"; + } + + + void MakeMissileArray() + { + Transform launchTransform = part.FindModelTransform(launchTransformName); + int missileNum = launchTransform.childCount; + launchTransforms = new Transform[missileNum]; + for (int i = 0; i < missileNum; i++) + { + string launcherName = launchTransform.GetChild(i).name; + int launcherIndex = int.Parse(launcherName.Substring(7)) - 1; //by coincidence, this is the same offset as rocket pods, which means the existing rocketlaunchers could potentially be converted over to homing munitions... + launchTransforms[launcherIndex] = launchTransform.GetChild(i); + } + salvoSize = Mathf.Min(salvoSize, launchTransforms.Length); + if (subMunitionPath != "") + { + PopulateMissileDummies(true); + } + } + public void PopulateMissileDummies(bool refresh = false) + { + if (refresh && displayOrdinance) + { + SetupMissileDummyPool(subMunitionPath); + foreach (var existingDummy in part.GetComponentsInChildren()) + { + existingDummy.Deactivate(); //if changing out missiles loaded into a VLS or similar, reset missile dummies + } + } + for (int i = 0; i < launchTransforms.Length; i++) + { + if (!refresh) + { + if (missileSpawner.ammoCount > i || isClusterMissile) + { + if (launchTransforms[i].localScale != Vector3.one) launchTransforms[i].localScale = Vector3.one; + } + tubesFired = 0; + } + else + { + if (!displayOrdinance) return; + GameObject dummy = mslDummyPool[subMunitionPath].GetPooledObject(); + MissileDummy dummyThis = dummy.GetComponentInChildren(); + dummyThis.AttachAt(part, launchTransforms[i]); + dummy.transform.localScale = dummyScale; + var mslAnim = dummy.GetComponentInChildren(); + if (mslAnim != null) mslAnim.enabled = false; + } + } + } + public void fireMissile(bool killWhenDone = false) + { + if (!HighLogic.LoadedSceneIsFlight) return; + if (isClusterMissile) salvoSize = launchTransforms.Length; + if (!(missileSalvo != null)) + { + missileSalvo = StartCoroutine(salvoFire(killWhenDone)); + wpm = VesselModuleRegistry.GetMissileFire(missileLauncher.SourceVessel, true); + if (useSymCounterpart) + { + using (List.Enumerator pSym = part.symmetryCounterparts.GetEnumerator()) + while (pSym.MoveNext()) + { + if (pSym.Current == null) continue; + if (pSym.Current != part && pSym.Current.vessel == vessel) + { + var ml = pSym.Current.FindModuleImplementing(); + if (ml == null) continue; + if (wpm != null) wpm.SendTargetDataToMissile(ml, false); + MissileLauncher launcher = ml as MissileLauncher; + if (launcher != null) + { + if (launcher.HasFired) continue; + launcher.FireMissile(); + } + } + } + } + } + } + IEnumerator salvoFire(bool LaunchThenDestroy) + { + int launchesThisSalvo = 0; + float timeGap = (60 / rippleRPM) * TimeWarp.CurrentRate; + int TargetID = 0; + bool missileRegistry = false; + //missileSpawner.MissileName = subMunitionName; + if (deployState != null) + { + deployState.enabled = true; + deployState.speed = 1; + yield return new WaitWhileFixed(() => deployState.normalizedTime < 1); //wait for animation here + deployState.normalizedTime = 1; + deployState.speed = 0; + deployState.enabled = false; + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.MultiMissileLauncher] deploy anim complete"); + } + for (int m = tubesFired; m < launchTransforms.Length; m++) + { + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MultiMissileLauncher] starting ripple launch on tube {m}, ripple delay: {timeGap:F3}"); + yield return new WaitForSecondsFixed(timeGap); + if (launchesThisSalvo >= salvoSize) //catch if launcher is trying to launch more missiles than it has + { + //if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.MultiMissileLauncher] oops! firing more missiles than tubes or ammo"); + break; + } + if (!isClusterMissile && (missileSpawner.ammoCount < 1 && !BDArmorySettings.INFINITE_ORDINANCE)) + { + tubesFired = 0; + break; + } + tubesFired++; + launchesThisSalvo++; + missileSpawner.SpawnMissile(launchTransforms[m], offset, !isClusterMissile); + MissileLauncher ml = missileSpawner.SpawnedMissile.FindModuleImplementing(); + yield return new WaitUntilFixed(() => ml.SetupComplete); // Wait until missile fully initialized. + var tnt = VesselModuleRegistry.GetModule(vessel, true); + if (tnt != null) + { + tnt.sourcevessel = missileLauncher.SourceVessel; + tnt.isMissile = true; + } + ml.Team = Team; + ml.SourceVessel = missileLauncher.SourceVessel; + if (string.IsNullOrEmpty(ml.GetShortName())) + { + ml.shortName = missileLauncher.GetShortName() + " Missile"; + } + ml.vessel.vesselName = ml.GetShortName(); + ml.TimeFired = Time.time; + if (!isClusterMissile) + ml.DetonationDistance = missileLauncher.DetonationDistance; + ml.DetonateAtMinimumDistance = missileLauncher.DetonateAtMinimumDistance; + ml.decoupleForward = missileLauncher.decoupleForward; + ml.dropTime = missileLauncher.dropTime; + ml.decoupleSpeed = missileLauncher.decoupleSpeed; + ml.guidanceActive = true; + ml.detonationTime = missileLauncher.detonationTime; + ml.engageAir = missileLauncher.engageAir; + ml.engageGround = missileLauncher.engageGround; + ml.engageMissile = missileLauncher.engageMissile; + ml.engageSLW = missileLauncher.engageSLW; + if (missileLauncher.GuidanceMode == GuidanceModes.AGMBallistic) + { + ml.BallisticOverShootFactor = missileLauncher.BallisticOverShootFactor; + ml.BallisticAngle = missileLauncher.BallisticAngle; + } + if (missileLauncher.GuidanceMode == GuidanceModes.Cruise) + { + ml.CruiseAltitude = missileLauncher.CruiseAltitude; + ml.CruiseSpeed = missileLauncher.CruiseSpeed; + ml.CruisePredictionTime = missileLauncher.CruisePredictionTime; + } + if (missileLauncher.GuidanceMode == GuidanceModes.AAMLoft) + { + ml.LoftMaxAltitude = missileLauncher.LoftMaxAltitude; + ml.LoftRangeOverride = missileLauncher.LoftRangeOverride; + ml.LoftAltitudeAdvMax = missileLauncher.LoftAltitudeAdvMax; + ml.LoftMinAltitude = missileLauncher.LoftMinAltitude; + ml.LoftAngle = missileLauncher.LoftAngle; + ml.LoftTermAngle = missileLauncher.LoftTermAngle; + ml.LoftRangeFac = missileLauncher.LoftRangeFac; + ml.LoftVelComp = missileLauncher.LoftVelComp; + ml.LoftVertVelComp = missileLauncher.LoftVertVelComp; + //ml.LoftAltComp = missileLauncher.LoftAltComp; + ml.terminalHomingRange = missileLauncher.terminalHomingRange; + ml.homingModeTerminal = missileLauncher.homingModeTerminal; + ml.pronavGain = missileLauncher.pronavGain; + ml.loftState = 0; + ml.TimeToImpact = float.PositiveInfinity; + ml.initMaxAoA = missileLauncher.maxAoA; + } + /*if (missileLauncher.GuidanceMode == GuidanceModes.AAMHybrid) + { + ml.pronavGain = missileLauncher.pronavGain; + ml.terminalHomingRange = missileLauncher.terminalHomingRange; + ml.homingModeTerminal = missileLauncher.homingModeTerminal; + }*/ + if (missileLauncher.GuidanceMode == GuidanceModes.APN || missileLauncher.GuidanceMode == GuidanceModes.PN) + ml.pronavGain = missileLauncher.pronavGain; + + ml.terminalHoming = missileLauncher.terminalHoming; + if (missileLauncher.terminalHoming) + { + if (missileLauncher.homingModeTerminal == GuidanceModes.AGMBallistic) + { + ml.BallisticOverShootFactor = missileLauncher.BallisticOverShootFactor; //are some of these null, and causeing this to quit? + ml.BallisticAngle = missileLauncher.BallisticAngle; + } + if (missileLauncher.homingModeTerminal == GuidanceModes.Cruise) + { + ml.CruiseAltitude = missileLauncher.CruiseAltitude; + ml.CruiseSpeed = missileLauncher.CruiseSpeed; + ml.CruisePredictionTime = missileLauncher.CruisePredictionTime; + } + if (missileLauncher.homingModeTerminal == GuidanceModes.AAMLoft) + { + ml.LoftMaxAltitude = missileLauncher.LoftMaxAltitude; + ml.LoftRangeOverride = missileLauncher.LoftRangeOverride; + ml.LoftAltitudeAdvMax = missileLauncher.LoftAltitudeAdvMax; + ml.LoftMinAltitude = missileLauncher.LoftMinAltitude; + ml.LoftAngle = missileLauncher.LoftAngle; + ml.LoftTermAngle = missileLauncher.LoftTermAngle; + ml.LoftRangeFac = missileLauncher.LoftRangeFac; + ml.LoftVelComp = missileLauncher.LoftVelComp; + ml.LoftVertVelComp = missileLauncher.LoftVertVelComp; + //ml.LoftAltComp = missileLauncher.LoftAltComp; + ml.pronavGain = missileLauncher.pronavGain; + ml.loftState = 0; + ml.TimeToImpact = float.PositiveInfinity; + ml.initMaxAoA = missileLauncher.maxAoA; + } + if (missileLauncher.homingModeTerminal == GuidanceModes.APN || missileLauncher.homingModeTerminal == GuidanceModes.PN) + ml.pronavGain = missileLauncher.pronavGain; + + ml.terminalHomingRange = missileLauncher.terminalHomingRange; + ml.homingModeTerminal = missileLauncher.homingModeTerminal; + ml.terminalHomingActive = false; + } + + //ml.decoupleSpeed = 5; + if (missileLauncher.GuidanceMode == GuidanceModes.AGM) + ml.maxAltitude = missileLauncher.maxAltitude; + ml.terminalGuidanceShouldActivate = missileLauncher.terminalGuidanceShouldActivate; + //if (isClusterMissile) ml.multiLauncher.overrideReferenceTransform = true; + if (ml.TargetingMode == MissileBase.TargetingModes.Heat || ml.TargetingMode == MissileBase.TargetingModes.Radar) + { + if (wpm.multiMissileTgtNum >= 2 && wpm != null) + { + if (TargetID > Mathf.Min((wpm.targetsAssigned.Count - 1), wpm.multiMissileTgtNum)) + { + TargetID = 0; //if more missiles than targets, loop target list + missileRegistry = true; + } + + if (wpm.targetsAssigned.Count > 0 && wpm.targetsAssigned[TargetID].Vessel != null) + { + if ((ml.engageAir && wpm.targetsAssigned[TargetID].isFlying) || + (ml.engageGround && wpm.targetsAssigned[TargetID].isLandedOrSurfaceSplashed) || + (ml.engageSLW && wpm.targetsAssigned[TargetID].isUnderwater)) //check engagement envelope + { + if (Vector3.Angle(wpm.targetsAssigned[TargetID].position - missileLauncher.MissileReferenceTransform.position, missileLauncher.GetForwardTransform()) < missileLauncher.maxOffBoresight) //is the target more-or-less in front of the missile(launcher)? + { + if (ml.TargetingMode == MissileBase.TargetingModes.Heat) //need to input a heattarget, else this will just return MissileFire.CurrentTarget + { + Vector3 direction = (wpm.targetsAssigned[TargetID].position * wpm.targetsAssigned[TargetID].velocity.magnitude) - missileLauncher.MissileReferenceTransform.position; + ml.heatTarget = BDATargetManager.GetHeatTarget(ml.SourceVessel, vessel, new Ray(missileLauncher.MissileReferenceTransform.position + (50 * missileLauncher.GetForwardTransform()), direction), TargetSignatureData.noTarget, ml.lockedSensorFOV * 0.5f, ml.heatThreshold, ml.frontAspectHeatModifier, true, ml.lockedSensorFOVBias, ml.lockedSensorVelocityBias, wpm, wpm.targetsAssigned[TargetID]); + } + if (ml.TargetingMode == MissileBase.TargetingModes.Radar) + { + //ml.radarLOAL = true; + ml.vrd = missileLauncher.vrd; //look into better method of assigning multiple radar targets - link into sourcevessel's vessleradardata.lockedtargetdata, iterate though target list? + TargetSignatureData[] scannedTargets = new TargetSignatureData[(int)wpm.multiMissileTgtNum]; + RadarUtils.RadarUpdateMissileLock(new Ray(ml.transform.position, ml.GetForwardTransform()), ml.lockedSensorFOV * 5, ref scannedTargets, 0.4f, ml); + TargetSignatureData lockedTarget = TargetSignatureData.noTarget; + + for (int i = 0; i < scannedTargets.Length; i++) + { + if (scannedTargets[i].exists && scannedTargets[i].vessel == wpm.targetsAssigned[TargetID].Vessel) + { + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log($"[BDArmory.MultiMissileLauncher] Found Radar target"); + ml.radarTarget = scannedTargets[i]; + break; + } + } + } + ml.targetVessel = wpm.targetsAssigned[TargetID]; + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log($"[BDArmory.MultiMissileLauncher] Assigning target {TargetID}: {wpm.targetsAssigned[TargetID].Vessel.GetName()}; total possible targets {wpm.targetsAssigned.Count}"); + } + else //else try remaining targets on the list. + { + for (int t = TargetID; t < wpm.targetsAssigned.Count; t++) + { + if ((ml.engageAir && !wpm.targetsAssigned[t].isFlying) || + (ml.engageGround && !wpm.targetsAssigned[t].isLandedOrSurfaceSplashed) || + (ml.engageSLW && !wpm.targetsAssigned[t].isUnderwater)) continue; //check engagement envelope + + if (Vector3.Angle(wpm.targetsAssigned[t].position - missileLauncher.MissileReferenceTransform.position, missileLauncher.GetForwardTransform()) < missileLauncher.maxOffBoresight) //is the target more-or-less in front of the missile(launcher)? + { + if (ml.TargetingMode == MissileBase.TargetingModes.Heat) + { + Vector3 direction = (wpm.targetsAssigned[t].position * wpm.targetsAssigned[t].velocity.magnitude) - missileLauncher.MissileReferenceTransform.position; + ml.heatTarget = BDATargetManager.GetHeatTarget(ml.SourceVessel, vessel, new Ray(missileLauncher.MissileReferenceTransform.position + (50 * missileLauncher.GetForwardTransform()), direction), TargetSignatureData.noTarget, ml.lockedSensorFOV * 0.5f, ml.heatThreshold, ml.frontAspectHeatModifier, true, ml.lockedSensorFOVBias, ml.lockedSensorVelocityBias, wpm, wpm.targetsAssigned[t]); + } + if (ml.TargetingMode == MissileBase.TargetingModes.Radar) + { + //ml.radarLOAL = true; + ml.vrd = missileLauncher.vrd; + TargetSignatureData[] scannedTargets = new TargetSignatureData[(int)wpm.multiMissileTgtNum]; + RadarUtils.RadarUpdateMissileLock(new Ray(ml.transform.position, ml.GetForwardTransform()), ml.lockedSensorFOV * 3, ref scannedTargets, 0.4f, ml); + TargetSignatureData lockedTarget = TargetSignatureData.noTarget; + + for (int i = 0; i < scannedTargets.Length; i++) + { + if (scannedTargets[i].exists && scannedTargets[i].vessel == wpm.targetsAssigned[TargetID].Vessel) + { + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log($"[BDArmory.MultiMissileLauncher] Found Radar target"); + ml.radarTarget = scannedTargets[i]; + break; + } + } + } + ml.targetVessel = wpm.targetsAssigned[t]; + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log($"[BDArmory.MultiMissileLauncher] Assigning backup target (targetID {TargetID}) {wpm.targetsAssigned[t].Vessel.GetName()}"); + } + } + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log($"[BDArmory.MultiMissileLauncher] Couldn't assign valid target, trying from beginning of target list"); + if (ml.targetVessel == null) //check targets that were already assigned and passed. using the above iterator to prevent all targets outisde allowed FoV or engagement enveolpe from being assigned the firest possible target by checking later ones first + { + using (List.Enumerator item = wpm.targetsAssigned.GetEnumerator()) + while (item.MoveNext()) + { + if (item.Current.Vessel == null) continue; + if (Vector3.Angle(item.Current.position - missileLauncher.MissileReferenceTransform.position, missileLauncher.GetForwardTransform()) < missileLauncher.maxOffBoresight) //is the target more-or-less in front of the missile(launcher)? + { + if (ml.TargetingMode == MissileBase.TargetingModes.Heat) + { + Vector3 direction = (item.Current.position * item.Current.velocity.magnitude) - missileLauncher.MissileReferenceTransform.position; + ml.heatTarget = BDATargetManager.GetHeatTarget(ml.SourceVessel, vessel, new Ray(missileLauncher.MissileReferenceTransform.position + (50 * missileLauncher.GetForwardTransform()), direction), TargetSignatureData.noTarget, ml.lockedSensorFOV * 0.5f, ml.heatThreshold, ml.frontAspectHeatModifier, true, ml.lockedSensorFOVBias, ml.lockedSensorVelocityBias, wpm, item.Current); + } + if (ml.TargetingMode == MissileBase.TargetingModes.Radar) + { + ml.radarLOAL = true; + ml.vrd = missileLauncher.vrd; + TargetSignatureData[] scannedTargets = new TargetSignatureData[(int)wpm.multiMissileTgtNum]; + RadarUtils.RadarUpdateMissileLock(new Ray(ml.transform.position, ml.GetForwardTransform()), ml.lockedSensorFOV * 3, ref scannedTargets, 0.4f, ml); + TargetSignatureData lockedTarget = TargetSignatureData.noTarget; + + for (int i = 0; i < scannedTargets.Length; i++) + { + if (scannedTargets[i].exists && scannedTargets[i].vessel == wpm.targetsAssigned[TargetID].Vessel) + { + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log($"[BDArmory.MultiMissileLauncher] Found Radar target"); + ml.radarTarget = scannedTargets[i]; + break; + } + } + } + ml.targetVessel = item.Current; + if (BDArmorySettings.DEBUG_MISSILES) + Debug.Log($"[BDArmory.MultiMissileLauncher] original target out of sensor range; engaging {item.Current.Vessel.GetName()}"); + break; + } + } + } + } + } + TargetID++; + } + } + else + { + if (wpm != null) wpm.SendTargetDataToMissile(ml, false); + } + } + else + { + if (wpm != null) wpm.SendTargetDataToMissile(ml); + } + if (!missileRegistry) + { + BDATargetManager.FiredMissiles.Add(ml); //so multi-missile salvoes only count as a single missile fired by the WM for maxMissilesPerTarget + } + ml.launched = true; + ml.TargetPosition = vessel.ReferenceTransform.position + (vessel.ReferenceTransform.up * 5000); //set initial target position so if no target update, missileBase will count a miss if it nears this point or is flying post-thrust + ml.MissileLaunch(); + launchTransforms[m].localScale = Vector3.zero; + } + wpm.heatTarget = TargetSignatureData.noTarget; + missileLauncher.launched = true; + if (deployState != null) + { + deployState.enabled = true; + deployState.speed = -1; + yield return new WaitWhileFixed(() => deployState.normalizedTime > 0); + deployState.normalizedTime = 0; + deployState.speed = 0; + deployState.enabled = false; + } + if (tubesFired >= launchTransforms.Length) //add a timer for reloading a partially emptied MML if it hasn't been used for a while? + { + if (!isClusterMissile && (BDArmorySettings.INFINITE_ORDINANCE || missileSpawner.ammoCount >= salvoSize)) + if (!(missileLauncher.reloadRoutine != null)) + { + missileLauncher.reloadRoutine = StartCoroutine(missileLauncher.MissileReload()); + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log("[BDArmory.MultiMissileLauncher] all submunitions fired. Reloading"); + } + } + missileLauncher.GetMissileCount(); + if (LaunchThenDestroy) + { + if (part != null) + { + missileLauncher.DestroyMissile(); + } + } + else + { + if (salvoSize < launchTransforms.Length && missileLauncher.reloadRoutine == null && (BDArmorySettings.INFINITE_ORDINANCE || missileSpawner.ammoCount > 0)) + { + if (launcherCooldown > 0) + { + missileLauncher.heatTimer = launcherCooldown; + yield return new WaitForSecondsFixed(launcherCooldown); + missileLauncher.launched = false; + missileLauncher.heatTimer = -1; + } + else + { + missileLauncher.heatTimer = -1; + missileLauncher.launched = false; + } + } + missileSalvo = null; + } + } + + public void SetupMissileDummyPool(string modelpath) + { + var key = modelpath; + if (!mslDummyPool.ContainsKey(key) || mslDummyPool[key] == null) + { + var Template = GameDatabase.Instance.GetModel(modelpath); + if (Template == null) + { + Debug.LogError("[BDArmory.MultiMissilelauncher]: model '" + modelpath + "' not found. Expect exceptions if trying to use this missile."); + return; + } + Template.SetActive(false); + Template.AddComponent(); + mslDummyPool[key] = ObjectPool.CreateObjectPool(Template, 10, true, true); + } + + } + public override string GetInfo() + { + StringBuilder output = new StringBuilder(); + + output.Append(Environment.NewLine); + output.AppendLine($"Multi Missile Launcher:"); + output.AppendLine($"- Salvo Size: {salvoSize}"); + output.AppendLine($"- Cooldown: {launcherCooldown} s"); + output.AppendLine($"- Salvo Size: {salvoSize}"); + output.AppendLine($" - Warhead:"); + AvailablePart missilePart = null; + using (var parts = PartLoader.LoadedPartsList.GetEnumerator()) + while (parts.MoveNext()) + { + //Debug.Log($"[BDArmory.MML]: Looking for {subMunitionName}"); + if (parts.Current.partConfig == null || parts.Current.partPrefab == null) + continue; + if (!parts.Current.partPrefab.partInfo.name.Contains(subMunitionName)) continue; + missilePart = parts.Current; + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MML]: found {missilePart.partPrefab.partInfo.name}"); + break; + } + if (missilePart != null) + { + var MML = (missilePart.partPrefab.FindModuleImplementing()); + if (MML != null) + { + if (MML.isClusterMissile) + { + output.AppendLine($"Cluster Missile:"); + output.AppendLine($"- SubMunition Count: {MML.salvoSize} "); + output.AppendLine($"- Blast radius: {Math.Round(BlastPhysicsUtils.CalculateBlastRange(tntMass), 2)} m"); + output.AppendLine($"- tnt Mass: {tntMass} kg"); + } + } + if (BDArmorySettings.DEBUG_MISSILES) Debug.Log($"[BDArmory.MML]: has BDExplosivePart: {missilePart.partPrefab.FindModuleImplementing()}"); + var ExplosivePart = (missilePart.partPrefab.FindModuleImplementing()); + if (ExplosivePart != null) + { + ExplosivePart.ParseWarheadType(); + if (missilePart.partPrefab.FindModuleImplementing()) + { + output.AppendLine($"Cluster Bomb:"); + output.AppendLine($"- Sub-Munition Count: {missilePart.partPrefab.FindModuleImplementing().submunitions.Count} "); + } + output.AppendLine($"- Blast radius: {Math.Round(BlastPhysicsUtils.CalculateBlastRange(ExplosivePart.tntMass), 2)} m"); + output.AppendLine($"- tnt Mass: {ExplosivePart.tntMass} kg"); + output.AppendLine($"- {ExplosivePart.warheadReportingName} warhead"); + } + var EMP = (missilePart.partPrefab.FindModuleImplementing()); + if (EMP != null) + { + output.AppendLine($"Electro-Magnetic Pulse"); + output.AppendLine($"- EMP Blast Radius: {EMP.proximity} m"); + } + var Nuke = (missilePart.partPrefab.FindModuleImplementing()); + if (Nuke != null) + { + float yield = Nuke.yield; + float radius = Nuke.thermalRadius; + float EMPRadius = Nuke.isEMP ? BDAMath.Sqrt(yield) * 500 : -1; + output.AppendLine($"- Yield: {yield} kT"); + output.AppendLine($"- Max radius: {radius} m"); + if (EMPRadius > 0) output.AppendLine($"- EMP Blast Radius: {EMPRadius} m"); + } + } + return output.ToString(); + } + } +} diff --git a/BDArmory/Modules/ModuleEMP.cs b/BDArmory/Weapons/ModuleEMP.cs similarity index 87% rename from BDArmory/Modules/ModuleEMP.cs rename to BDArmory/Weapons/ModuleEMP.cs index d63a1612d..ec09a24ba 100644 --- a/BDArmory/Modules/ModuleEMP.cs +++ b/BDArmory/Weapons/ModuleEMP.cs @@ -1,7 +1,9 @@ using System.Text; -using UnityEngine; -namespace BDArmory.Modules +using BDArmory.Damage; +using BDArmory.Utils; + +namespace BDArmory.Weapons { public class ModuleEMP : PartModule { @@ -9,6 +11,9 @@ public class ModuleEMP : PartModule UI_Label(affectSymCounterparts = UI_Scene.All, controlEnabled = true, scene = UI_Scene.All)] public float proximity = 5000; + [KSPField] + public bool AllowReboot = false; + public override void OnStart(StartState state) { if (HighLogic.LoadedSceneIsFlight) @@ -23,6 +28,7 @@ public void DetonateEMPRoutine() { foreach (Vessel v in FlightGlobals.Vessels) { + if (v == null || !v.loaded || v.packed) continue; if (VesselModuleRegistry.ignoredVesselTypes.Contains(v.vesselType)) continue; if (!v.HoldPhysics) { @@ -36,7 +42,7 @@ public void DetonateEMPRoutine() emp = (ModuleDrainEC)v.rootPart.AddModule("ModuleDrainEC"); } emp.incomingDamage += ((proximity - (float)targetDistance) * 10); //this way craft at edge of blast might only get disabled instead of bricked - emp.softEMP = false; //can bypass DMP damage cap + emp.softEMP = AllowReboot; //can bypass DMP damage cap } } } diff --git a/BDArmory/Modules/ModuleWeapon.cs b/BDArmory/Weapons/ModuleWeapon.cs similarity index 64% rename from BDArmory/Modules/ModuleWeapon.cs rename to BDArmory/Weapons/ModuleWeapon.cs index d7f443647..55040e472 100644 --- a/BDArmory/Modules/ModuleWeapon.cs +++ b/BDArmory/Weapons/ModuleWeapon.cs @@ -1,4 +1,3 @@ -using KSP.Localization; using KSP.UI.Screens; using System.Collections.Generic; using System.Collections; @@ -10,18 +9,20 @@ using BDArmory.Bullets; using BDArmory.Competition; using BDArmory.Control; -using BDArmory.Core.Extension; -using BDArmory.Core.Module; -using BDArmory.Core.Utils; -using BDArmory.Core; +using BDArmory.Damage; +using BDArmory.Extensions; using BDArmory.FX; using BDArmory.GameModes; -using BDArmory.Misc; +using BDArmory.ModIntegration; using BDArmory.Radar; +using BDArmory.Settings; using BDArmory.Targeting; using BDArmory.UI; +using BDArmory.Utils; +using BDArmory.Weapons.Missiles; +using BDArmory.WeaponMounts; -namespace BDArmory.Modules +namespace BDArmory.Weapons { public class ModuleWeapon : EngageableWeapon, IBDWeapon { @@ -36,6 +37,7 @@ public class ModuleWeapon : EngageableWeapon, IBDWeapon Coroutine shutdownRoutine; Coroutine standbyRoutine; Coroutine reloadRoutine; + Coroutine chargeRoutine; bool finalFire; @@ -46,7 +48,7 @@ public class ModuleWeapon : EngageableWeapon, IBDWeapon public enum WeaponTypes { Ballistic, - Rocket, //Cannon's depreciated, lets use this for rocketlaunchers + Rocket, //Cannon's deprecated, lets use this for rocketlaunchers Laser } @@ -57,7 +59,8 @@ public enum WeaponStates PoweringUp, PoweringDown, Locked, - Standby // Not currently firing, but can still track the current target. + Standby, // Not currently firing, but can still track the current target. + EnabledForSecondaryFiring // Enabled, but only for secondary firing. } public enum BulletDragTypes @@ -78,6 +81,12 @@ public enum FuzeTypes Impact //standard contact + graze fuze, detonates on hit //Laser //laser-guided smart rounds? } + public enum FillerTypes + { + None, //No HE filler, non-explosive slug. + Standard, //standard HE filler for a standard exposive shell + Shaped //shaped charge filler, for HEAT rounds and similar + } public enum APSTypes { Ballistic, @@ -96,13 +105,15 @@ public enum APSTypes public FuzeTypes eFuzeType; + public FillerTypes eHEType; + public APSTypes eAPSType; public float heat; public bool isOverheated; - private bool isRippleFiring = false;//used to tell when weapon has started firing for initial ripple delay - + private bool isRippleFiring = false;//used to tell when weapon has started firing for initial ripple delay + private bool wasFiring; //used for knowing when to stop looped audio clip (when you're not shooting, but you were) @@ -116,7 +127,7 @@ public enum APSTypes private BDStagingAreaGauge gauge; private int AmmoID; - + private int ECID; //AI public bool aiControlled = false; public bool autoFire; @@ -134,22 +145,19 @@ public float targetAdjustedMaxCosAngle { get { - var fireTransform = (eWeaponType == WeaponTypes.Rocket && rocketPod) ? rockets[0].parent : fireTransforms[0]; + var fireTransform = (eWeaponType == WeaponTypes.Rocket && rocketPod) ? (rockets[0] != null ? rockets[0].parent : null) : fireTransforms != null ? fireTransforms[0] : null; + if (fireTransform == null) return 1f; var theta = FiringTolerance * targetRadius / (finalAimTarget - fireTransform.position).magnitude + Mathf.Deg2Rad * maxDeviation / 2f; // Approximation to arctan(α*r/d) + θ/2. (arctan(x) = x-x^3/3 + O(x^5)) - return finalAimTarget.IsZero() ? 1f : 1f - 0.5f * theta * theta; // Approximation to cos(theta). (cos(x) = 1-x^2/2!+O(x^4)) + return finalAimTarget.IsZero() ? 1f : Mathf.Max(1f - 0.5f * theta * theta, 0); // Approximation to cos(theta). (cos(x) = 1-x^2/2!+O(x^4)) } } public Vector3 targetPosition; - private Vector3 targetVelocity; // local frame velocity - private Vector3 targetAcceleration; // local frame - private Vector3 targetVelocityPrevious; // for acceleration calculation - // private Vector3 targetAccelerationPrevious; + public Vector3 targetVelocity; // local frame velocity + public Vector3 targetAcceleration; // local frame private Vector3 targetVelocityS1; private Vector3 targetVelocityS2; private Vector3 targetAccelerationS1; private Vector3 targetAccelerationS2; - private bool targetAccelerationWasReset = true; - // private Vector3 relativeVelocity; public Vector3 finalAimTarget; Vector3 lastFinalAimTarget; public Vessel visualTargetVessel; @@ -169,18 +177,20 @@ public float targetAdjustedMaxCosAngle public bool targetEngines = false; public bool targetWeapons = false; public bool targetMass = false; + public bool targetRandom = false; - int layerMask1 = (int)(LayerMasks.Parts | LayerMasks.Scenery | LayerMasks.EVA | LayerMasks.Unknown19 | LayerMasks.Unknown23); // Why 19 and 23? - int layerMask2 = (int)(LayerMasks.Parts | LayerMasks.Scenery | LayerMasks.Unknown19); // Why 19 and why not the other layer mask? - - enum TargetAcquisitionType { None, Visual, Slaved, Radar, AutoProxy }; + RaycastHit[] laserHits = new RaycastHit[100]; + Collider[] heatRayColliders = new Collider[100]; + const int layerMask1 = (int)(LayerMasks.Parts | LayerMasks.Scenery | LayerMasks.EVA | LayerMasks.Unknown19 | LayerMasks.Unknown23 | LayerMasks.Wheels); // Why 19 and 23? + const int layerMask2 = (int)(LayerMasks.Parts | LayerMasks.Scenery | LayerMasks.Unknown19 | LayerMasks.Wheels); // Why 19 and why not the other layer mask? + enum TargetAcquisitionType { None, Visual, Slaved, Radar, AutoProxy, GPS }; TargetAcquisitionType targetAcquisitionType = TargetAcquisitionType.None; TargetAcquisitionType lastTargetAcquisitionType = TargetAcquisitionType.None; float lastGoodTargetTime = 0; public Vector3? FiringSolutionVector => finalAimTarget.IsZero() ? (Vector3?)null : (finalAimTarget - fireTransforms[0].position).normalized; - public bool recentlyFiring //used by guard to know if it should evaid this + public bool recentlyFiring //used by guard to know if it should evade this { get { return Time.time - timeFired < 1; } } @@ -193,6 +203,7 @@ enum TargetAcquisitionType { None, Visual, Slaved, Radar, AutoProxy }; //aimer textures Vector3 pointingAtPosition; + float pointingDistance = 500f; Vector3 bulletPrediction; Vector3 fixedLeadOffset = Vector3.zero; @@ -224,6 +235,8 @@ public MissileFire weaponManager bool userFiring; Vector3 laserPoint; public bool slaved; + public bool GPSTarget; + public bool radarTarget; public Transform turretBaseTransform { @@ -285,7 +298,7 @@ public Part GetPart() { //using (List.Enumerator craftPart = vessel.parts.GetEnumerator()) //{ - ammoLeft = "Ammo Left: " + ammoCount.ToString("0"); + ammoLeft = $"Ammo Left: {ammoCount:0}"; int lastAmmoID = this.AmmoID; using (var weapon = VesselModuleRegistry.GetModules(vessel).GetEnumerator()) while (weapon.MoveNext()) @@ -295,7 +308,7 @@ public Part GetPart() if (weapon.Current.AmmoID != this.AmmoID && weapon.Current.AmmoID != lastAmmoID) { vessel.GetConnectedResourceTotals(weapon.Current.AmmoID, out double ammoCurrent, out double ammoMax); - ammoLeft += "; " + ammoCurrent.ToString("0"); + ammoLeft += $"; {ammoCurrent:0}"; lastAmmoID = weapon.Current.AmmoID; } } @@ -307,23 +320,37 @@ public string GetMissileType() return string.Empty; } + public string GetPartName() + { + return WeaponName; + } + + public float GetEngageRange() + { + return engageRangeMax; + } + public bool resourceSteal = false; public float strengthMutator = 1; public bool instagib = false; -#if DEBUG Vector3 debugTargetPosition; Vector3 debugLastTargetPosition; Vector3 debugRelVelAdj; Vector3 debugAccAdj; Vector3 debugGravAdj; -#endif + Vector3 debugCorrection; + Vector3 debugSimCPA; + Vector3 debugBulletPred; + Vector3 debugTargetPred; #endregion Declarations #region KSPFields [KSPField(isPersistant = true, guiActive = true, guiName = "#LOC_BDArmory_WeaponName", guiActiveEditor = true), UI_Label(affectSymCounterparts = UI_Scene.All, scene = UI_Scene.All)]//Weapon Name + public string WeaponDisplayName; + public string WeaponName; [KSPField] @@ -351,6 +378,13 @@ public string GetMissileType() public string reloadAnimName = "reloadAnim"; AnimationState reloadState; + [KSPField] + public bool hasChargeAnimation = false; + + [KSPField] + public string chargeAnimName = "chargeAnim"; + AnimationState chargeState; + [KSPField] public bool hasFireAnimation = false; @@ -373,7 +407,7 @@ public string GetMissileType() public bool BurstOverride = false; [KSPField(advancedTweakable = true, isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_FiringBurstCount"),//Burst Firing Count - UI_FloatRange(minValue = 1f, maxValue = 100f, stepIncrement = 1, scene = UI_Scene.All)] + UI_FloatRange(minValue = 1f, maxValue = 100f, stepIncrement = 1, scene = UI_Scene.All, affectSymCounterparts = UI_Scene.All)] public float fireBurstLength = 1; [KSPField(isPersistant = true)] @@ -392,9 +426,9 @@ public string GetMissileType() [KSPField(isPersistant = true, guiActive = false, guiActiveEditor = true, guiName = "Rate of Fire"), UI_FloatRange(minValue = 100f, maxValue = 1500, stepIncrement = 25f, scene = UI_Scene.Editor, affectSymCounterparts = UI_Scene.All)] - public float roundsPerMinute = 650; //rocket RoF slider + public float roundsPerMinute = 650; //RoF slider - public float baseRPM; + public float baseRPM = 650; [KSPField] public bool isChaingun = false; //does the gun have adjustable RoF @@ -406,6 +440,9 @@ public string GetMissileType() [KSPField] public float maxEffectiveDistance = 2500; //used by AI to select appropriate weapon + [KSPField] + public float minSafeDistance = 0; //used by AI to select appropriate weapon + [KSPField] public float bulletMass = 0.3880f; //mass in KG - used for damage and recoil and drag @@ -451,6 +488,13 @@ public string GetMissileType() [KSPField] public bool BurstFire = false; // set to true for weapons that fire multiple times per triggerpull + [KSPField] + public float ChargeTime = -1; + bool isCharging = false; + [KSPField] + public bool ChargeEachShot = true; + bool hasCharged = false; + [KSPField] public string bulletDragTypeName = "AnalyticEstimate"; public BulletDragTypes bulletDragType; @@ -540,8 +584,8 @@ public string GetMissileType() public float tntMass = 0; - public bool ImpulseInConfig = false; //record if impulse weapon in config for resetting weapons post mutator - public bool GraviticInConfig = false; //record if gravitic weapon in config for resetting weapons post mutator + //public bool ImpulseInConfig = false; //record if impulse weapon in config for resetting weapons post mutator + //public bool GraviticInConfig = false; //record if gravitic weapon in config for resetting weapons post mutator //public List attributeList; public bool explosive = false; @@ -655,7 +699,7 @@ public string GetMissileType() public string overheatSoundPath = "BDArmory/Parts/50CalTurret/sounds/turretOverheat"; [KSPField] - public string chargeSoundPath = "BDArmory/Parts/laserTest/sounds/charge"; + public string chargeSoundPath = "BDArmory/Parts/ABL/sounds/charge"; [KSPField] public string rocketSoundPath = "BDArmory/Sounds/rocketLoop"; @@ -708,7 +752,7 @@ public void ToggleAmmoConfig() if (advancedAmmoOption == true) { - Events["ToggleAmmoConfig"].guiName = Localizer.Format("#LOC_BDArmory_advanced");//"Advanced Ammo Config" + Events["ToggleAmmoConfig"].guiName = StringUtils.Localize("#LOC_BDArmory_advanced");//"Advanced Ammo Config" Events["ConfigAmmo"].guiActive = true; Events["ConfigAmmo"].guiActiveEditor = true; Fields["AmmoTypeNum"].guiActive = false; @@ -716,14 +760,14 @@ public void ToggleAmmoConfig() } else { - Events["ToggleAmmoConfig"].guiName = Localizer.Format("#LOC_BDArmory_simple");//"Simple Ammo Config + Events["ToggleAmmoConfig"].guiName = StringUtils.Localize("#LOC_BDArmory_simple");//"Simple Ammo Config Events["ConfigAmmo"].guiActive = false; Events["ConfigAmmo"].guiActiveEditor = false; Fields["AmmoTypeNum"].guiActive = true; Fields["AmmoTypeNum"].guiActiveEditor = true; useCustomBelt = false; } - Utils.RefreshAssociatedWindows(part); + GUIUtils.RefreshAssociatedWindows(part); } [KSPField(advancedTweakable = true, isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_useBelt")]//Using Custom Loadout public bool useCustomBelt = false; @@ -747,7 +791,7 @@ public void ConfigAmmo() int AmmoIntervalCounter = 0; [KSPField(guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_Ammo_LoadedAmmo")]//Status - public string guiAmmoTypeString = Localizer.Format("#LOC_BDArmory_Ammo_Slug"); + public string guiAmmoTypeString = StringUtils.Localize("#LOC_BDArmory_Ammo_Slug"); [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_DeployableWeapon"), // In custom/modded "cargo bay" UI_ChooseOption( @@ -798,11 +842,13 @@ public void ConfigAmmo() //auto proximity tracking [KSPField] public float autoProxyTrackRange = 0; - bool atprAcquired; + public bool atprAcquired; int aptrTicker; - float timeFired; + public float timeFired; public float initialFireDelay = 0; //used to ripple fire multiple weapons of this type + float InitialFireDelay => weaponManager.barrageStagger > 0 ? initialFireDelay * weaponManager.barrageStagger : initialFireDelay; + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_Barrage")]//Barrage public bool useRippleFire = true; @@ -831,24 +877,82 @@ public void ToggleRipple() [KSPField(isPersistant = true)] public bool isAPS = false; + [KSPField(isPersistant = true)] + public bool dualModeAPS = false; + [KSPField] public string APSType = "missile"; //missile/ballistic/omni + private float delayTime = -1; + IEnumerator IncrementRippleIndex(float delay) { - if (isRippleFiring) delay = 0; + if (isRippleFiring) delay = 0; if (delay > 0) { - yield return new WaitForSeconds(delay); + yield return new WaitForSecondsFixed(delay); } - if (weaponManager == null) yield break; - weaponManager.gunRippleIndex = weaponManager.gunRippleIndex + 1; + if (weaponManager == null || weaponManager.vessel != this.vessel) yield break; + weaponManager.incrementRippleIndex(WeaponName); //Debug.Log("[BDArmory.ModuleWeapon]: incrementing ripple index to: " + weaponManager.gunRippleIndex); } int barrelIndex = 0; + int animIndex = 0; + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_CustomFireKey"), UI_Label(scene = UI_Scene.All)] + public string customFireKey = ""; + BDInputInfo CustomFireKey; + [KSPEvent(guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_SetCustomFireKey")] // Set Custom Fire Key + void SetCustomFireKey() + { + if (!bindingKey) + StartCoroutine(BindCustomFireKey()); + } + bool bindingKey = false; + IEnumerator BindCustomFireKey() + { + Events["SetCustomFireKey"].guiName = StringUtils.Localize("#LOC_BDArmory_InputSettings_recordedInput"); + bindingKey = true; + int id = 0; + BDKeyBinder.BindKey(id); + while (bindingKey) + { + if (BDKeyBinder.IsRecordingID(id)) + { + string recordedInput; + if (BDKeyBinder.current.AcquireInputString(out recordedInput)) + { + if (recordedInput == "escape") // Clear the binding + SetCustomFireKey(""); + else if (recordedInput != "mouse 0") // Left clicking cancels + SetCustomFireKey(recordedInput); + bindingKey = false; + break; + } + } + else + { + bindingKey = false; + break; + } + yield return null; + } + Events["SetCustomFireKey"].guiName = StringUtils.Localize("#LOC_BDArmory_SetCustomFireKey"); + } + public void SetCustomFireKey(string key, bool applySym = true) + { + CustomFireKey = new BDInputInfo(key, "Custom Fire Key"); + customFireKey = CustomFireKey.inputString; + if (!applySym) return; + using (List.Enumerator sym = part.symmetryCounterparts.GetEnumerator()) + while (sym.MoveNext()) + { + if (sym.Current == null) continue; + sym.Current.FindModuleImplementing().SetCustomFireKey(key, false); + } + } #endregion KSPFields #region KSPActions @@ -893,7 +997,7 @@ public void AGFireHold(KSPActionParam param) IEnumerator FireHoldRoutine(KSPActionGroup group) { - KeyBinding key = Utils.AGEnumToKeybinding(group); + KeyBinding key = OtherUtils.AGEnumToKeybinding(group); if (key == null) { yield break; @@ -908,6 +1012,7 @@ IEnumerator FireHoldRoutine(KSPActionGroup group) agHoldFiring = false; yield break; } + [KSPEvent(guiActive = true, guiName = "#LOC_BDArmory_Jettison", active = true, guiActiveEditor = false)]//Jettison public void Jettison() // make rocketpods jettisonable { @@ -919,6 +1024,11 @@ public void Jettison() // make rocketpods jettisonable if (BDArmorySetup.Instance.ActiveWeaponManager != null) BDArmorySetup.Instance.ActiveWeaponManager.UpdateList(); } + [KSPAction("Jettison")] // Give them an action group too. + public void AGJettison(KSPActionParam param) + { + Jettison(); + } #endregion KSPActions #region KSP Events @@ -941,21 +1051,28 @@ public void Start() ParseWeaponType(weaponType); // extension for feature_engagementenvelope + if (dualModeAPS) isAPS = true; if (isAPS) { - HideEngageOptions(); - Events["ShowUI"].active = false; - Events["HideUI"].active = false; - Events["Toggle"].active = false; + if (!dualModeAPS) + { + HideEngageOptions(); + Events["ShowUI"].active = false; + Events["HideUI"].active = false; + Events["Toggle"].active = false; + Fields["priority"].guiActive = false; + Fields["priority"].guiActiveEditor = false; + } ParseAPSType(APSType); } - InitializeEngagementRange(0, maxEffectiveDistance); + InitializeEngagementRange(minSafeDistance, maxEffectiveDistance); if (string.IsNullOrEmpty(GetShortName())) { shortName = part.partInfo.title; } OriginalShortName = shortName; - WeaponName = shortName; + WeaponDisplayName = shortName; + WeaponName = part.partInfo.name; //have weaponname be the .cfg part name, since not all weapons have a shortName in the .cfg using (var emitter = part.FindModelComponents().AsEnumerable().GetEnumerator()) while (emitter.MoveNext()) { @@ -963,7 +1080,20 @@ public void Start() emitter.Current.emit = false; EffectBehaviour.AddParticleEmitter(emitter.Current); } - baseRPM = roundsPerMinute; + + if (eWeaponType != WeaponTypes.Laser || (eWeaponType == WeaponTypes.Laser && pulseLaser)) + { + try + { + baseRPM = float.Parse(ConfigNodeUtils.FindPartModuleConfigNodeValue(part.partInfo.partConfig, "ModuleWeapon", "roundsPerMinute")); + } + catch + { + baseRPM = 3000; + Debug.LogError($"[BDArmory.ModuleWeapon] {shortName} missing roundsPerMinute field in .cfg! Fix your .cfg!"); + } + } + else baseRPM = 3000; if (roundsPerMinute >= 1500 || (eWeaponType == WeaponTypes.Laser && !pulseLaser)) { @@ -1036,7 +1166,6 @@ public void Start() Fields["FiringTolerance"].guiActiveEditor = FireAngleOverride; Fields["fireBurstLength"].guiActive = BurstOverride; Fields["fireBurstLength"].guiActiveEditor = BurstOverride; - vessel.Velocity(); if (BurstFire) { BeltFed = false; @@ -1112,6 +1241,11 @@ public void Start() { if (mtf.Current == null) continue; KSPParticleEmitter kpe = mtf.Current.GetComponent(); + if (kpe == null) + { + Debug.LogError("[BDArmory.ModuleWeapon] MuzzleFX transform missing KSPParticleEmitter component. Please fix your model"); + continue; + } EffectBehaviour.AddParticleEmitter(kpe); muzzleFlashEmitters.Add(kpe); kpe.emit = false; @@ -1134,26 +1268,26 @@ public void Start() { if (!string.IsNullOrEmpty(ammoBelt) && ammoBelt != "def") { + var validAmmoTypes = BDAcTools.ParseNames(bulletType); + if (validAmmoTypes.Count == 0) + { + Debug.LogError($"[BDArmory.ModuleWeapon]: Weapon {WeaponName} has no valid ammo types! Reverting to 'def'."); + validAmmoTypes = new List { "def" }; + } customAmmoBelt = BDAcTools.ParseNames(ammoBelt); - List testAmmo = BDAcTools.ParseNames(bulletType); - for (int i = 0; i < customAmmoBelt.Count; i++) + for (int i = 0; i < customAmmoBelt.Count; ++i) { - bool validAmmo = false; - for (int t = 0; t < testAmmo.Count; t++) + if (!validAmmoTypes.Contains(customAmmoBelt[i])) { - if (customAmmoBelt[i].Contains(testAmmo[t])) - { - validAmmo = true; - break; - } - if (!validAmmo) customAmmoBelt[i] = testAmmo[0]; + Debug.LogWarning($"[BDArmory.ModuleWeapon] Invalid ammo type {customAmmoBelt[i]} at position {i} in ammo belt of {WeaponName} on {vessel.vesselName}! reverting to valid ammo type {validAmmoTypes[0]}"); + customAmmoBelt[i] = validAmmoTypes[0]; } } baseBulletVelocity = BulletInfo.bullets[customAmmoBelt[0].ToString()].bulletVelocity; } - else + else //belt is empty/"def" reset useAmmoBelt { - customAmmoBelt = BDAcTools.ParseNames(bulletType); + useCustomBelt = false; } } } @@ -1175,6 +1309,7 @@ public void Start() //setup transforms fireTransforms = part.FindModelTransforms(fireTransformName); + if (fireTransforms.Length == 0) Debug.LogError("[BDArmory.ModuleWeapon] Weapon missing fireTransform [" + fireTransformName + "]! Please fix your model"); shellEjectTransforms = part.FindModelTransforms(shellEjectTransformName); //setup emitters @@ -1201,10 +1336,10 @@ public void Start() } //setup projectile colors - projectileColorC = Utils.ParseColor255(projectileColor); + projectileColorC = GUIUtils.ParseColor255(projectileColor); endColorS = projectileColor.Split(","[0]); - startColorC = Utils.ParseColor255(startColor); + startColorC = GUIUtils.ParseColor255(startColor); startColorS = startColor.Split(","[0]); //init and zero points @@ -1214,16 +1349,20 @@ public void Start() //setup audio SetupAudio(); - + if (eWeaponType == WeaponTypes.Laser || ChargeTime > 0) + { + chargeSound = SoundUtils.GetAudioClip(chargeSoundPath); + } // Setup gauges gauge = (BDStagingAreaGauge)part.AddModule("BDStagingAreaGauge"); gauge.AmmoName = ammoName; - gauge.AudioSource = audioSource; - gauge.ReloadAudioClip = reloadAudioClip; - gauge.ReloadCompleteAudioClip = reloadCompleteAudioClip; - - AmmoID = PartResourceLibrary.Instance.GetDefinition(ammoName).id; + var AmmoDef = PartResourceLibrary.Instance.GetDefinition(ammoName); + if (AmmoDef != null) + AmmoID = AmmoDef.id; + else + Debug.LogError($"[BDArmory.ModuleWeapon]: Resource definition for {ammoName} not found!"); + ECID = PartResourceLibrary.Instance.GetDefinition("ElectricCharge").id; // This should always be found. //laser setup if (eWeaponType == WeaponTypes.Laser) { @@ -1253,8 +1392,21 @@ public void Start() else if (HighLogic.LoadedSceneIsEditor) { fireTransforms = part.FindModelTransforms(fireTransformName); + if (fireTransforms.Length == 0) Debug.LogError("[BDArmory.ModuleWeapon] Weapon missing fireTransform [" + fireTransformName + "]! Please fix your model"); WeaponNameWindow.OnActionGroupEditorOpened.Add(OnActionGroupEditorOpened); WeaponNameWindow.OnActionGroupEditorClosed.Add(OnActionGroupEditorClosed); + if (useCustomBelt) + { + if (!string.IsNullOrEmpty(ammoBelt) && ammoBelt != "def") + { + customAmmoBelt = BDAcTools.ParseNames(ammoBelt); + baseBulletVelocity = BulletInfo.bullets[customAmmoBelt[0].ToString()].bulletVelocity; + } + else + { + useCustomBelt = false; + } + } } //turret setup List.Enumerator turr = part.FindModulesImplementing().GetEnumerator(); @@ -1278,43 +1430,77 @@ public void Start() if ((turret || eWeaponType != WeaponTypes.Rocket) || (eWeaponType == WeaponTypes.Rocket && (!rocketPod || (rocketPod && externalAmmo)))) { Events["Jettison"].guiActive = false; + Actions["AGJettison"].active = false; } } //setup animations if (hasDeployAnim) { - deployState = Utils.SetUpSingleAnimation(deployAnimName, part); - deployState.normalizedTime = 0; - deployState.speed = 0; - deployState.enabled = true; - ReloadAnimTime = (ReloadTime - deployState.length); + deployState = GUIUtils.SetUpSingleAnimation(deployAnimName, part); + if (deployState != null) + { + deployState.normalizedTime = 0; + deployState.speed = 0; + deployState.enabled = true; + ReloadAnimTime = (ReloadTime - deployState.length); + } + else + { + Debug.LogWarning($"[BDArmory.ModuleWeapon]: {OriginalShortName} is missing deploy anim"); + hasDeployAnim = false; + } } if (hasReloadAnim) { - reloadState = Utils.SetUpSingleAnimation(reloadAnimName, part); - reloadState.normalizedTime = 0; - reloadState.speed = 0; - reloadState.enabled = true; + reloadState = GUIUtils.SetUpSingleAnimation(reloadAnimName, part); + if (reloadState != null) + { + reloadState.normalizedTime = 1; + reloadState.speed = 0; + reloadState.enabled = true; + } + else + { + Debug.LogWarning($"[BDArmory.ModuleWeapon]: {OriginalShortName} is missing reload anim"); + hasReloadAnim = false; + } + } + if (hasChargeAnimation) + { + chargeState = GUIUtils.SetUpSingleAnimation(chargeAnimName, part); + if (chargeState != null) + { + chargeState.normalizedTime = 0; + chargeState.speed = 0; + chargeState.enabled = true; + } + else + { + Debug.LogWarning($"[BDArmory.ModuleWeapon]: {OriginalShortName} is missing charge anim"); + hasChargeAnimation = false; + } } if (hasFireAnimation) { List animList = BDAcTools.ParseNames(fireAnimName); - fireState = new AnimationState[animList.Count]; //this should become animList.Count, for cases where there's a bultibarrel weapon with a single fireanim - for (int i = 0; i < fireTransforms.Length; i++) + //animList = animList.OrderBy(w => w).ToList(); + fireState = new AnimationState[animList.Count]; + //for (int i = 0; i < fireTransforms.Length; i++) + for (int i = 0; i < animList.Count; i++) { try { - fireState[i] = Utils.SetUpSingleAnimation(animList[i].ToString(), part); + fireState[i] = GUIUtils.SetUpSingleAnimation(animList[i].ToString(), part); //Debug.Log("[BDArmory.ModuleWeapon] Added fire anim " + i); - fireState[i].enabled = false; + fireState[i].normalizedTime = 0; } catch { - Debug.Log("[BDArmory.ModuleWeapon] Missing fire anim " + i); + Debug.LogWarning($"[BDArmory.ModuleWeapon]: {OriginalShortName} is missing fire anim " + i); } } } - + /* if (graviticWeapon) { GraviticInConfig = true; @@ -1322,7 +1508,7 @@ public void Start() if (impulseWeapon) { ImpulseInConfig = true; - } + }*/ if (eWeaponType != WeaponTypes.Laser) { SetupAmmo(null, null); @@ -1331,26 +1517,50 @@ public void Start() { if (rocketInfo == null) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log("[BDArmory.ModuleWeapon]: Failed To load rocket : " + currentType); + //if (BDArmorySettings.DEBUG_WEAPONS) + Debug.LogWarning("[BDArmory.ModuleWeapon]: Failed To load rocket : " + currentType); } else { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_WEAPONS) Debug.Log("[BDArmory.ModuleWeapon]: AmmoType Loaded : " + currentType); + if (beehive) + { + if (!BulletInfo.bulletNames.Contains(rocketInfo.subMunitionType) || !RocketInfo.rocketNames.Contains(rocketInfo.subMunitionType)) + { + beehive = false; + Debug.LogWarning("[BDArmory.ModuleWeapon]: Invalid submunition on : " + currentType); + } + else + { + if (RocketInfo.rocketNames.Contains(rocketInfo.subMunitionType)) + { + RocketInfo sRocket = RocketInfo.rockets[rocketInfo.subMunitionType]; + SetupRocketPool(sRocket.name, sRocket.rocketModelPath); //Will need to move this if rockets ever get ammobelt functionality + } + } + } } } else { if (bulletInfo == null) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) - Debug.Log("[BDArmory.ModuleWeapon]: Failed To load bullet : " + currentType); + //if (BDArmorySettings.DEBUG_WEAPONS) + Debug.LogWarning("[BDArmory.ModuleWeapon]: Failed To load bullet : " + currentType); } else { - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_WEAPONS) Debug.Log("[BDArmory.ModuleWeapon]: BulletType Loaded : " + currentType); + if (beehive) + { + if (!BulletInfo.bulletNames.Contains(bulletInfo.subMunitionType)) + { + beehive = false; + Debug.LogWarning("[BDArmory.ModuleWeapon]: Invalid submunition on : " + currentType); + } + } } } } @@ -1358,6 +1568,25 @@ public void Start() BDArmorySetup.OnVolumeChange += UpdateVolume; if (HighLogic.LoadedSceneIsFlight) { TimingManager.FixedUpdateAdd(TimingManager.TimingStage.FashionablyLate, AimAndFire); } + CustomFireKey = new BDInputInfo(customFireKey, "Custom Fire"); + + if (HighLogic.LoadedSceneIsFlight) + { + if (isAPS) + { + EnableWeapon(); + } + } + + if (BDArmorySettings.RUNWAY_PROJECT_ROUND == 59) + { + if (WeaponName == "bahaTurret") + { + maxEffectiveDistance = 1000; + InitializeEngagementRange(minSafeDistance, 1000); + engageRangeMax = 1000; + } + } } void OnDestroy() @@ -1393,47 +1622,68 @@ public void PAWRefresh() Fields["detonationRange"].guiActive = false; Fields["detonationRange"].guiActiveEditor = false; } - Utils.RefreshAssociatedWindows(part); + GUIUtils.RefreshAssociatedWindows(part); } [KSPEvent(advancedTweakable = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_FireAngleOverride_Enable", active = true)]//Disable fire angle override public void ToggleOverrideAngle() { - FireAngleOverride = !FireAngleOverride; - - if (FireAngleOverride == false) - { - Events["ToggleOverrideAngle"].guiName = Localizer.Format("#LOC_BDArmory_FireAngleOverride_Enable");// Enable Firing Angle Override - } - else - { - Events["ToggleOverrideAngle"].guiName = Localizer.Format("#LOC_BDArmory_FireAngleOverride_Disable");// Disable Firing Angle Override - } + using (List.Enumerator craftPart = EditorLogic.fetch.ship.parts.GetEnumerator()) + while (craftPart.MoveNext()) + { + if (craftPart.Current == null) continue; + if (craftPart.Current.name != part.name) continue; + using (List.Enumerator weapon = craftPart.Current.FindModulesImplementing().GetEnumerator()) + while (weapon.MoveNext()) + { + if (weapon.Current == null) continue; + weapon.Current.FireAngleOverride = !weapon.Current.FireAngleOverride; + if (weapon.Current.FireAngleOverride == false) + { + weapon.Current.Events["ToggleOverrideAngle"].guiName = StringUtils.Localize("#LOC_BDArmory_FireAngleOverride_Enable");// Enable Firing Angle Override + } + else + { + weapon.Current.Events["ToggleOverrideAngle"].guiName = StringUtils.Localize("#LOC_BDArmory_FireAngleOverride_Disable");// Disable Firing Angle Override + } - Fields["FiringTolerance"].guiActive = FireAngleOverride; - Fields["FiringTolerance"].guiActiveEditor = FireAngleOverride; + weapon.Current.Fields["FiringTolerance"].guiActive = weapon.Current.FireAngleOverride; + weapon.Current.Fields["FiringTolerance"].guiActiveEditor = weapon.Current.FireAngleOverride; - Utils.RefreshAssociatedWindows(part); + GUIUtils.RefreshAssociatedWindows(weapon.Current.part); + } + } } [KSPEvent(advancedTweakable = true, guiActive = true, guiActiveEditor = true, guiName = "#LOC_BDArmory_BurstLengthOverride_Enable", active = true)]//Burst length override public void ToggleBurstLengthOverride() { - BurstOverride = !BurstOverride; - - if (BurstOverride == false) - { - Events["ToggleBurstLengthOverride"].guiName = Localizer.Format("#LOC_BDArmory_BurstLengthOverride_Enable");// Enable Burst Fire length Override - } - else - { - Events["ToggleBurstLengthOverride"].guiName = Localizer.Format("#LOC_BDArmory_BurstLengthOverride_Disable");// Disable Burst Fire length Override - } + using (List.Enumerator craftPart = EditorLogic.fetch.ship.parts.GetEnumerator()) + while (craftPart.MoveNext()) + { + if (craftPart.Current == null) continue; + if (craftPart.Current.name != part.name) continue; + using (List.Enumerator weapon = craftPart.Current.FindModulesImplementing().GetEnumerator()) + while (weapon.MoveNext()) + { + if (weapon.Current == null) continue; + weapon.Current.BurstOverride = !weapon.Current.BurstOverride; + if (weapon.Current.BurstOverride == false) + { + weapon.Current.Events["ToggleBurstLengthOverride"].guiName = StringUtils.Localize("#LOC_BDArmory_BurstLengthOverride_Enable");// Enable Firing Angle Override + } + else + { + weapon.Current.Events["ToggleBurstLengthOverride"].guiName = StringUtils.Localize("#LOC_BDArmory_BurstLengthOverride_Disable");// Disable Firing Angle Override + } - Fields["fireBurstLength"].guiActive = BurstOverride; - Fields["fireBurstLength"].guiActiveEditor = BurstOverride; + weapon.Current.Fields["fireBurstLength"].guiActive = weapon.Current.BurstOverride; + weapon.Current.Fields["fireBurstLength"].guiActiveEditor = weapon.Current.BurstOverride; - Utils.RefreshAssociatedWindows(part); + GUIUtils.RefreshAssociatedWindows(weapon.Current.part); + } + } } + void FAOCos(BaseField field, object obj) { maxAutoFireCosAngle = Mathf.Cos((FiringTolerance * Mathf.Deg2Rad)); @@ -1459,6 +1709,10 @@ public string WeaponStatusdebug() return status; } + + bool fireConditionCheck => ((((userFiring || agHoldFiring) && !isAPS) || autoFire) && (!turret || turret.TargetInRange(finalAimTarget, 10, float.MaxValue))) || (BurstFire && RoundsRemaining > 0 && RoundsRemaining < RoundsPerMag); + //if user pulling the trigger || AI controlled and on target if turreted || finish a burstfire weapon's burst + void Update() { if (HighLogic.LoadedSceneIsFlight && FlightGlobals.ready && !vessel.packed && vessel.IsControllable) @@ -1475,11 +1729,20 @@ void Update() } } - if (weaponState == WeaponStates.Enabled && (TimeWarp.WarpMode != TimeWarp.Modes.HIGH || TimeWarp.CurrentRate == 1)) + var secondaryFireKeyActive = false; + if ((vessel.isActiveVessel || BDArmorySettings.REMOTE_SHOOTING) && !MapView.MapIsEnabled && !aiControlled) + { + secondaryFireKeyActive = BDInputUtils.GetKey(CustomFireKey); + if (secondaryFireKeyActive) EnableWeapon(secondaryFiring: true); + else if (weaponState == WeaponStates.EnabledForSecondaryFiring) StandbyWeapon(); + } + + if ((weaponState == WeaponStates.Enabled || weaponState == WeaponStates.EnabledForSecondaryFiring) && (TimeWarp.WarpMode != TimeWarp.Modes.HIGH || TimeWarp.CurrentRate == 1)) { - userFiring = (BDInputUtils.GetKey(BDInputSettingsFields.WEAP_FIRE_KEY) && (vessel.isActiveVessel || BDArmorySettings.REMOTE_SHOOTING) && !MapView.MapIsEnabled && !aiControlled); - if (!(((userFiring || agHoldFiring) && !isAPS) || (autoFire && //if user pulling the trigger || AI controlled and on target if turreted || finish a burstfire weapon's burst - (!turret || turret.TargetInRange(finalAimTarget, 10, float.MaxValue))) || (BurstFire && RoundsRemaining > 0 && RoundsRemaining < RoundsPerMag))) + userFiring = (((weaponState == WeaponStates.Enabled && BDInputUtils.GetKey(BDInputSettingsFields.WEAP_FIRE_KEY) && !GUIUtils.CheckMouseIsOnGui()) //don't fire if mouse on WM GUI; Issue #348 + || secondaryFireKeyActive) + && (vessel.isActiveVessel || BDArmorySettings.REMOTE_SHOOTING) && !MapView.MapIsEnabled && !aiControlled); + if (!fireConditionCheck) { if (spinDownAnimation) spinningDown = true; //this doesn't need to be called every fixed frame and can remain here if (!oneShotSound && wasFiring) //technically the laser reset stuff could also have remained here @@ -1508,7 +1771,6 @@ void Update() fireAnimSpeed = Mathf.Lerp(fireAnimSpeed, 0, 0.04f); } } - // Draw gauges if (vessel.isActiveVessel) { @@ -1540,7 +1802,7 @@ void FixedUpdate() { if (!(weaponState == WeaponStates.PoweringDown || weaponState == WeaponStates.Disabled)) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.ModuleWeapon]: Vessel is uncontrollable, disabling weapon " + part.name); + if (BDArmorySettings.DEBUG_WEAPONS) Debug.Log($"[BDArmory.ModuleWeapon]: Vessel {vessel.vesselName} is uncontrollable, disabling weapon " + part.name); DisableWeapon(); } return; @@ -1548,7 +1810,7 @@ void FixedUpdate() UpdateHeat(); if (weaponState == WeaponStates.Standby && (TimeWarp.WarpMode != TimeWarp.Modes.HIGH || TimeWarp.CurrentRate == 1)) { aimOnly = true; } - if (weaponState == WeaponStates.Enabled && (TimeWarp.WarpMode != TimeWarp.Modes.HIGH || TimeWarp.CurrentRate == 1)) + if ((weaponState == WeaponStates.Enabled || weaponState == WeaponStates.EnabledForSecondaryFiring) && (TimeWarp.WarpMode != TimeWarp.Modes.HIGH || TimeWarp.CurrentRate == 1)) { aimAndFireIfPossible = true; // Aim and fire in a later timing phase of FixedUpdate. This synchronises firing with the physics instead of waiting until the scene is rendered. It also occurs before Krakensbane adjustments have been made (in the Late timing phase). } @@ -1608,31 +1870,32 @@ public void ShowUI() void OnGUI() { - if (HighLogic.LoadedSceneIsFlight && weaponState == WeaponStates.Enabled && vessel && !vessel.packed && vessel.isActiveVessel && - BDArmorySettings.DRAW_AIMERS && !aiControlled && !MapView.MapIsEnabled && !pointingAtSelf && !isAPS) + if (trajectoryRenderer != null && (!BDArmorySettings.DEBUG_LINES || !(weaponState == WeaponStates.Enabled || weaponState == WeaponStates.EnabledForSecondaryFiring || weaponState == WeaponStates.Standby))) { trajectoryRenderer.enabled = false; } + if (HighLogic.LoadedSceneIsFlight && (weaponState == WeaponStates.Enabled || weaponState == WeaponStates.EnabledForSecondaryFiring) && vessel && !vessel.packed && vessel.isActiveVessel && + BDArmorySettings.DRAW_AIMERS && (MouseAimFlight.IsMouseAimActive || !aiControlled) && !MapView.MapIsEnabled && !pointingAtSelf && !isAPS) { float size = 30; Vector3 reticlePosition; if (BDArmorySettings.AIM_ASSIST) { - if (targetAcquired && (slaved || yawRange < 1 || maxPitch - minPitch < 1)) + if (targetAcquired && (GPSTarget || slaved || yawRange < 1 || maxPitch - minPitch < 1)) { - reticlePosition = pointingAtPosition + fixedLeadOffset; + if (BDArmorySettings.AIM_ASSIST_MODE) // Target + reticlePosition = pointingAtPosition + fixedLeadOffset / targetDistance * pointingDistance; + else // Aimer + reticlePosition = transform.position + (finalAimTarget - transform.position).normalized * pointingDistance; - if (!slaved) + if (!slaved && !GPSTarget) { - BDGUIUtils.DrawLineBetweenWorldPositions(pointingAtPosition, reticlePosition, 2, - new Color(0, 1, 0, 0.6f)); + GUIUtils.DrawLineBetweenWorldPositions(pointingAtPosition, reticlePosition, 2, new Color(0, 1, 0, 0.6f)); } - BDGUIUtils.DrawTextureOnWorldPos(pointingAtPosition, BDArmorySetup.Instance.greenDotTexture, - new Vector2(6, 6), 0); + GUIUtils.DrawTextureOnWorldPos(pointingAtPosition, BDArmorySetup.Instance.greenDotTexture, new Vector2(6, 6), 0); if (atprAcquired) { - BDGUIUtils.DrawTextureOnWorldPos(targetPosition, BDArmorySetup.Instance.openGreenSquare, - new Vector2(20, 20), 0); + GUIUtils.DrawTextureOnWorldPos(targetPosition, BDArmorySetup.Instance.openGreenSquare, new Vector2(20, 20), 0); } } else @@ -1654,14 +1917,13 @@ void OnGUI() { texture = BDArmorySetup.Instance.greenPointCircleTexture; } - BDGUIUtils.DrawTextureOnWorldPos(reticlePosition, texture, new Vector2(size, size), 0); + GUIUtils.DrawTextureOnWorldPos(reticlePosition, texture, new Vector2(size, size), 0); - if (BDArmorySettings.DRAW_DEBUG_LINES) + if (BDArmorySettings.DEBUG_LINES) { if (targetAcquired) { - BDGUIUtils.DrawLineBetweenWorldPositions(fireTransforms[0].position, targetPosition, 2, - Color.blue); + GUIUtils.DrawLineBetweenWorldPositions(fireTransforms[0].position, targetPosition, 2, Color.blue); } } } @@ -1671,16 +1933,21 @@ void OnGUI() DrawAlignmentIndicator(); } -#if DEBUG - if (BDArmorySettings.DRAW_DEBUG_LINES && weaponState == WeaponStates.Enabled && vessel && !vessel.packed && !MapView.MapIsEnabled) + if (BDArmorySettings.DEBUG_LINES && BDArmorySettings.DEBUG_WEAPONS && (weaponState == WeaponStates.Enabled || weaponState == WeaponStates.EnabledForSecondaryFiring) && vessel && !vessel.packed && !MapView.MapIsEnabled) { - BDGUIUtils.MarkPosition(debugTargetPosition, transform, Color.cyan); - BDGUIUtils.DrawLineBetweenWorldPositions(debugTargetPosition, debugTargetPosition + debugRelVelAdj, 2, Color.green); - BDGUIUtils.DrawLineBetweenWorldPositions(debugTargetPosition + debugRelVelAdj, debugTargetPosition + debugRelVelAdj + debugAccAdj, 2, Color.magenta); - BDGUIUtils.DrawLineBetweenWorldPositions(debugTargetPosition + debugRelVelAdj + debugAccAdj, debugTargetPosition + debugRelVelAdj + debugAccAdj + debugGravAdj, 2, Color.yellow); - BDGUIUtils.MarkPosition(finalAimTarget, transform, Color.cyan, size: 4); + GUIUtils.MarkPosition(debugTargetPosition, transform, Color.grey); //lets not have two MarkPositions use the same color... + GUIUtils.DrawLineBetweenWorldPositions(debugTargetPosition, debugTargetPosition + debugRelVelAdj, 2, Color.green); + GUIUtils.DrawLineBetweenWorldPositions(debugTargetPosition + debugRelVelAdj, debugTargetPosition + debugRelVelAdj + debugAccAdj, 2, Color.magenta); + GUIUtils.DrawLineBetweenWorldPositions(debugTargetPosition + debugRelVelAdj + debugAccAdj, debugTargetPosition + debugRelVelAdj + debugAccAdj + debugGravAdj, 2, Color.yellow); + if (!debugCorrection.IsZero()) + { + GUIUtils.DrawLineBetweenWorldPositions(debugTargetPosition + debugRelVelAdj + debugAccAdj + debugGravAdj, debugTargetPosition + debugRelVelAdj + debugAccAdj + debugGravAdj - debugCorrection, 2, Color.red); + GUIUtils.MarkPosition(debugSimCPA, transform, Color.red, size: 2); + GUIUtils.MarkPosition(debugBulletPred, transform, Color.yellow, size: 2); + GUIUtils.MarkPosition(debugTargetPred, transform, Color.green, size: 2); + } + GUIUtils.MarkPosition(finalAimTarget, transform, Color.cyan, size: 4); } -#endif } #endregion KSP Events @@ -1701,15 +1968,15 @@ private void Fire() if (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 41) timeGap = ((60 / BDArmorySettings.FIRE_RATE_OVERRIDE) * fireTransforms.Length) * TimeWarp.CurrentRate; - if (useRippleFire) + if (useRippleFire && fireState.Length > 1) { - timeGap /= fireTransforms.Length; //to maintain RPM if only firing one barrel at a time + timeGap /= fireTransforms.Length; //to maintain RPM if only firing one barrel at a time - This should only be being called on guns with multiple fireanims(and thus multiple independant barrels); is causing twinlinked weapons to gain 2x firespeed in barrageMode } if (Time.time - timeFired > timeGap && !isOverheated && !isReloading && !pointingAtSelf - && (aiControlled || !Utils.CheckMouseIsOnGui()) + && (aiControlled || !GUIUtils.CheckMouseIsOnGui()) && WMgrAuthorized()) { bool effectsShot = false; @@ -1728,7 +1995,9 @@ private void Fire() //recoil if (hasRecoil) { - part.rb.AddForceAtPosition((-fireTransform.forward) * (bulletVelocity * (bulletMass * ProjectileCount) / 1000 * BDArmorySettings.RECOIL_FACTOR * recoilReduction), + //doesn't take propellant gass mass into account; GAU-8 should be 44kN, yields 29.9; Vulc should be 14.2, yields ~10.4; GAU-22 16.5, yields 11.9 + //Adding a mult of 1.4 brings the GAU8 to 41.8, Vulc to 14.5, GAU-22 to 16.6; not exact, but a reasonably close approximation that looks to scale consistantly across ammos + part.rb.AddForceAtPosition(((-fireTransform.forward) * (bulletVelocity * (bulletMass * ProjectileCount) / 1000) * 1.4f * BDArmorySettings.RECOIL_FACTOR * recoilReduction), fireTransform.position, ForceMode.Impulse); } @@ -1751,7 +2020,22 @@ private void Fire() pBullet.caliber = bulletInfo.caliber; pBullet.bulletVelocity = bulletInfo.bulletVelocity; pBullet.bulletMass = bulletInfo.bulletMass; - pBullet.explosive = bulletInfo.explosive; + if (bulletInfo.tntMass > 0) + { + switch (eHEType) + { + case FillerTypes.Standard: + pBullet.HEType = PooledBullet.PooledBulletTypes.Explosive; + break; + case FillerTypes.Shaped: + pBullet.HEType = PooledBullet.PooledBulletTypes.Shaped; + break; + } + } + else + { + pBullet.HEType = PooledBullet.PooledBulletTypes.Slug; + } pBullet.incendiary = bulletInfo.incendiary; pBullet.apBulletMod = bulletInfo.apBulletMod; pBullet.bulletDmgMult = bulletDmgMult; @@ -1772,15 +2056,20 @@ private void Fire() pBullet.timeToLiveUntil = Mathf.Max(maxTargetingRange, maxEffectiveDistance) / bulletVelocity * 1.1f + Time.time; timeFired = Time.time - iTime; + if (isRippleFiring && weaponManager.barrageStagger > 0) // Add variability to fired time to cause variability in reload time. + { + var reloadVariability = UnityEngine.Random.Range(-weaponManager.barrageStagger, weaponManager.barrageStagger); + timeFired += reloadVariability; + } Vector3 firedVelocity = VectorUtils.GaussianDirectionDeviation(fireTransform.forward, (maxDeviation / 2)) * bulletVelocity; - pBullet.currentVelocity = (part.rb.velocity + Krakensbane.GetFrameVelocityV3f()) + firedVelocity; // use the real velocity, w/o offloading - // pBullet.transform.position += (part.rb.velocity + Krakensbane.GetFrameVelocityV3f()) * Time.fixedDeltaTime; // Initially, put the bullet at the fireTransform position at the start of the next physics frame + pBullet.currentVelocity = (part.rb.velocity + BDKrakensbane.FrameVelocityV3f) + firedVelocity; // use the real velocity, w/o offloading pBullet.sourceWeapon = this.part; pBullet.sourceVessel = vessel; pBullet.team = weaponManager.Team.Name; pBullet.bulletTexturePath = bulletTexturePath; + pBullet.projectileColor = projectileColorC; pBullet.startColor = startColorC; pBullet.fadeColor = fadeColor; @@ -1844,11 +2133,14 @@ private void Fire() } pBullet.EMP = bulletInfo.EMP; pBullet.nuclear = bulletInfo.nuclear; - pBullet.beehive = bulletInfo.beehive; - if (bulletInfo.beehive) pBullet.subMunitionType = BulletInfo.bullets[bulletInfo.subMunitionType]; + pBullet.beehive = beehive; + if (bulletInfo.beehive) + { + pBullet.subMunitionType = BulletInfo.bullets[bulletInfo.subMunitionType]; + } //pBullet.homing = BulletInfo.homing; - pBullet.impulse = bulletInfo.impulse; - pBullet.massMod = bulletInfo.massMod; + pBullet.impulse = Impulse; + pBullet.massMod = massAdjustment; switch (bulletDragType) { case BulletDragTypes.None: @@ -1876,16 +2168,24 @@ private void Fire() pBullet.isAPSprojectile = true; pBullet.tgtShell = tgtShell; pBullet.tgtRocket = tgtRocket; + if (delayTime > -1) pBullet.timeToLiveUntil = delayTime; } + BDACompetitionMode.Instance.Scores.RegisterShot(vessel.GetName()); pBullet.gameObject.SetActive(true); if (!pBullet.CheckBulletCollisions(iTime)) // Check that the bullet won't immediately hit anything. { - pBullet.MoveBullet(iTime); // Move the bullet forward by the amount of time within the physics frame determined by it's firing rate. - } - else - { - // Debug.Log("DEBUG immediately hit after " + pBullet.DistanceTraveled + "m and time " + iTime); + // This requires some trickery since we're moving it within the same frame so the Krakensbane frame velocity and the part velocity need to be dealt with separately. + // The following gets bullet tracers to line up properly when at orbital velocities. + var bVel = pBullet.currentVelocity; + var pVel = part.rb.velocity; + var kbVel = BDKrakensbane.FrameVelocityV3f; + pBullet.currentVelocity = firedVelocity; + pBullet.MoveBullet(iTime); // Move the bullet forward by the amount of time within the physics frame determined by it's firing rate. Note: the default is 1 frame and reduces to 0. + pBullet.currentVelocity = bVel; + if (kbVel.IsZero()) pBullet.transform.position += pVel * Time.fixedDeltaTime; + pBullet.SetTracerPosition(); + pBullet.transform.position += (pVel + kbVel) * Time.fixedDeltaTime; } } //heat @@ -1911,30 +2211,30 @@ private void Fire() } } - if (useRippleFire) + if (fireState.Length > 1) { - if (fireState.Length > 1) //need to add clause for singlebarrel guns + barrelIndex++; + animIndex++; + //Debug.Log("[BDArmory.ModuleWeapon]: barrelIndex for " + this.GetShortName() + " is " + barrelIndex + "; total barrels " + fireTransforms.Length); + if ((!BurstFire || (BurstFire && (RoundsRemaining >= RoundsPerMag))) && barrelIndex + 1 > fireTransforms.Length) //only advance ripple index if weapon isn't burstfire, has finished burst, or has fired with all barrels { - barrelIndex++; - //Debug.Log("[BDArmory.ModuleWeapon]: barrelIndex for " + this.GetShortName() + " is " + barrelIndex + "; total barrels " + fireTransforms.Length); - if ((!BurstFire || (BurstFire && (RoundsRemaining >= RoundsPerMag))) && barrelIndex + 1 > fireTransforms.Length) //only advance ripple index if weapon isn't brustfire, has finished burst, or has fired with all barrels + StartCoroutine(IncrementRippleIndex(InitialFireDelay * TimeWarp.CurrentRate)); + isRippleFiring = true; + if (barrelIndex >= fireTransforms.Length) { - StartCoroutine(IncrementRippleIndex(initialFireDelay * TimeWarp.CurrentRate)); - isRippleFiring = true; - if (barrelIndex + 1 > fireTransforms.Length) - { - barrelIndex = 0; - //Debug.Log("[BDArmory.ModuleWeapon]: barrelIndex for " + this.GetShortName() + " reset"); - } + barrelIndex = 0; + //Debug.Log("[BDArmory.ModuleWeapon]: barrelIndex for " + this.GetShortName() + " reset"); } } - else + if (animIndex >= fireState.Length) animIndex = 0; + } + else + { + if (!BurstFire || (BurstFire && (RoundsRemaining >= RoundsPerMag))) { - if (!BurstFire || (BurstFire && (RoundsRemaining >= RoundsPerMag))) - { - StartCoroutine(IncrementRippleIndex(initialFireDelay * TimeWarp.CurrentRate)); //this is why ripplefire is slower, delay to stagger guns should only be being called once - isRippleFiring = true; - } + StartCoroutine(IncrementRippleIndex(InitialFireDelay * TimeWarp.CurrentRate)); //this is why ripplefire is slower, delay to stagger guns should only be being called once + isRippleFiring = true; + //need to know what next weapon in ripple sequence is, and have firedelay be set to whatever it's RPM is, not this weapon's or a generic average } } } @@ -1973,13 +2273,13 @@ private bool FireLaser() if (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 41 && !isAPS) timeGap = ((60 / BDArmorySettings.FIRE_RATE_OVERRIDE) * fireTransforms.Length) * TimeWarp.CurrentRate; - if (useRippleFire) + if (useRippleFire && fireState.Length > 1) { timeGap /= fireTransforms.Length; //to maintain RPM if only firing one barrel at a time } beamDuration = Math.Min(timeGap * 0.8f, 0.1f); if ((!pulseLaser || ((Time.time - timeFired > timeGap) && pulseLaser)) - && !pointingAtSelf && !Utils.CheckMouseIsOnGui() && WMgrAuthorized() && !isOverheated) // && !isReloading) + && !pointingAtSelf && !GUIUtils.CheckMouseIsOnGui() && WMgrAuthorized() && !isOverheated) // && !isReloading) { if (CanFire(chargeAmount)) { @@ -1993,30 +2293,30 @@ private bool FireLaser() LaserBeam(aName); } heat += heatPerShot; - if (useRippleFire) + + if (fireState.Length > 1) { - if (fireState.Length > 1) //need to add clause for singlebarrel guns + barrelIndex++; + animIndex++; + //Debug.Log("[BDArmory.ModuleWeapon]: barrelIndex for " + this.GetShortName() + " is " + barrelIndex + "; total barrels " + fireTransforms.Length); + if ((!BurstFire || (BurstFire && (RoundsRemaining >= RoundsPerMag))) && barrelIndex + 1 > fireTransforms.Length) //only advance ripple index if weapon isn't brustfire, has finished burst, or has fired with all barrels { - barrelIndex++; - //Debug.Log("[BDArmory.ModuleWeapon]: barrelIndex for " + this.GetShortName() + " is " + barrelIndex + "; total barrels " + fireTransforms.Length); - if ((!BurstFire || (BurstFire && (RoundsRemaining >= RoundsPerMag))) && barrelIndex + 1 > fireTransforms.Length) //only advance ripple index if weapon isn't brustfire, has finished burst, or has fired with all barrels + StartCoroutine(IncrementRippleIndex(InitialFireDelay * TimeWarp.CurrentRate)); + isRippleFiring = true; + if (barrelIndex >= fireTransforms.Length) { - StartCoroutine(IncrementRippleIndex(initialFireDelay * TimeWarp.CurrentRate)); - isRippleFiring = true; - if (barrelIndex + 1 > fireTransforms.Length) - { - barrelIndex = 0; - //Debug.Log("[BDArmory.ModuleWeapon]: barrelIndex for " + this.GetShortName() + " reset"); - } + barrelIndex = 0; + //Debug.Log("[BDArmory.ModuleWeapon]: barrelIndex for " + this.GetShortName() + " reset"); } } - else + if (animIndex >= fireState.Length) animIndex = 0; + } + else + { + if (!BurstFire || (BurstFire && (RoundsRemaining >= RoundsPerMag))) { - if (!BurstFire || (BurstFire && (RoundsRemaining >= RoundsPerMag))) - { - StartCoroutine(IncrementRippleIndex(initialFireDelay * TimeWarp.CurrentRate)); - isRippleFiring = true; - } + StartCoroutine(IncrementRippleIndex(InitialFireDelay * TimeWarp.CurrentRate)); + isRippleFiring = true; } } } @@ -2080,21 +2380,28 @@ private void LaserBeam(string vesselname) rayDirection = VectorUtils.GaussianDirectionDeviation(tf.forward, maxDeviation / 2); targetDirectionLR = rayDirection.normalized; } - else if (((((visualTargetVessel != null && visualTargetVessel.loaded) || slaved) || (isAPS && (tgtShell != null || tgtRocket != null))) && (turret && (turret.yawRange > 0 && turret.maxPitch > 0))) // causes laser to snap to target CoM if close enough. changed to only apply to turrets + /*else if (((((visualTargetVessel != null && visualTargetVessel.loaded) || slaved) || (isAPS && (tgtShell != null || tgtRocket != null))) && (turret && (turret.yawRange > 0 && turret.maxPitch > 0))) // causes laser to snap to target CoM if close enough. changed to only apply to turrets && Vector3.Angle(rayDirection, targetDirection) < (isAPS ? 1f : 0.25f)) //if turret and within .25 deg (or 1 deg if APS), snap to target { //targetDirection = targetPosition + (relativeVelocity * Time.fixedDeltaTime) * 2 - tf.position; - targetDirection = targetPosition - tf.position; + targetDirection = targetPosition - tf.position; //something in here is throwing off the laser aim, causing the beam to be fired wildly off-target. Disabling it for now. FIXME - debug this later rayDirection = targetDirection; targetDirectionLR = targetDirection.normalized; - } + }*/ Ray ray = new Ray(tf.position, rayDirection); lr.useWorldSpace = false; lr.SetPosition(0, Vector3.zero); - var hits = Physics.RaycastAll(ray, maxTargetingRange, layerMask1); - if (hits.Length > 0) // Find the first valid hit. + + var hitCount = Physics.RaycastNonAlloc(ray, laserHits, maxTargetingRange, layerMask1); + if (hitCount == laserHits.Length) // If there's a whole bunch of stuff in the way (unlikely), then we need to increase the size of our hits buffer. + { + laserHits = Physics.RaycastAll(ray, maxTargetingRange, layerMask1); + hitCount = laserHits.Length; + } + //Debug.Log($"[LASER DEBUG] hitCount: {hitCount}"); + if (hitCount > 0) { - var orderedHits = hits.OrderBy(x => x.distance); + var orderedHits = laserHits.Take(hitCount).OrderBy(x => x.distance); using (var hitsEnu = orderedHits.GetEnumerator()) { while (hitsEnu.MoveNext()) @@ -2117,117 +2424,141 @@ private void LaserBeam(string vesselname) if (p && p.vessel && p.vessel != vessel) { float distance = hit.distance; - //Scales down the damage based on the increased surface area of the area being hit by the laser. Think flashlight on a wall. - if (electroLaser) + if (instagib) { - var mdEC = p.vessel.rootPart.FindModuleImplementing(); - if (mdEC == null) - { - p.vessel.rootPart.AddModule("ModuleDrainEC"); - } - var emp = p.vessel.rootPart.FindModuleImplementing(); - if (!pulseLaser) - { - emp.incomingDamage += (ECPerShot / 1000); - } - else - { - emp.incomingDamage += (ECPerShot / 20); - } - emp.softEMP = true; + p.AddInstagibDamage(); + ExplosionFx.CreateExplosion(hit.point, + (1), "BDArmory/Models/explosion/explosion", explSoundPath, ExplosionSourceType.Bullet, 0, null, vessel.vesselName, null); } - else if (impulseWeapon) + else { - if (!pulseLaser) - { - damage = Impulse * TimeWarp.fixedDeltaTime; - } - else - { - damage = Impulse; - } - if (p.rb != null && p.rb.mass > 0) + if (electroLaser || HeatRay) { - if (Impulse > 0) + if (electroLaser) { - p.rb.AddForceAtPosition((p.transform.position - tf.position).normalized * (float)damage, p.transform.position, ForceMode.Acceleration); + var mdEC = p.vessel.rootPart.FindModuleImplementing(); + if (mdEC == null) + { + p.vessel.rootPart.AddModule("ModuleDrainEC"); + } + var emp = p.vessel.rootPart.FindModuleImplementing(); + float EMPDamage = 0; + if (!pulseLaser) + { + EMPDamage = ECPerShot / 1000; + emp.incomingDamage += EMPDamage; + } + else + { + EMPDamage = ECPerShot / 20; + emp.incomingDamage += EMPDamage; + } + emp.softEMP = true; + damage = EMPDamage; + if (BDArmorySettings.DEBUG_WEAPONS) Debug.Log($"[BDArmory.ModuleWeapon]: EMP Buildup Applied to {p.vessel.GetName()}: {(pulseLaser ? (ECPerShot / 20) : (ECPerShot / 1000))}"); } else { - p.rb.AddForceAtPosition((tf.position - p.transform.position).normalized * (float)damage, p.transform.position, ForceMode.Acceleration); + var dist = Mathf.Sin(maxDeviation) * (tf.position - laserPoint).magnitude; + var hitCount2 = Physics.OverlapSphereNonAlloc(hit.point, dist, heatRayColliders, layerMask2); + if (hitCount2 == heatRayColliders.Length) + { + heatRayColliders = Physics.OverlapSphere(hit.point, dist, layerMask2); + hitCount2 = heatRayColliders.Length; + } + using (var hitsEnu2 = heatRayColliders.Take(hitCount2).GetEnumerator()) + { + while (hitsEnu2.MoveNext()) + { + KerbalEVA kerb = hitsEnu2.Current.gameObject.GetComponentUpwards(); + Part hitP = kerb ? kerb.part : hitsEnu2.Current.GetComponentInParent(); + if (hitP == null) continue; + if (ProjectileUtils.IsIgnoredPart(hitP)) continue; + if (hitP && hitP != p && hitP.vessel && hitP.vessel != vessel) + { + //p.AddDamage(damage); + p.AddSkinThermalFlux(damage); //add modifier to adjust damage by armor diffusivity value + } + } + if (BDArmorySettings.DEBUG_WEAPONS) Debug.Log($"[BDArmory.ModuleWeapon]: Heatray Applying {damage} heat to target"); + } } } - } - else - { - HitpointTracker armor = p.GetComponent(); - initialDamage = (laserDamage / (1 + Mathf.PI * Mathf.Pow(tanAngle * distance, 2)) * 0.425f); - - if (armor != null)// technically, lasers shouldn't do damage until armor gone, but that would require localized armor tracking instead of the monolithic model currently used - { - damage = (initialDamage * (pulseLaser ? 1 : TimeWarp.fixedDeltaTime)) * Mathf.Clamp((1 - (Mathf.Sqrt(armor.Diffusivity * (armor.Density / 1000)) * armor.ArmorThickness) / initialDamage), 0.005f, 1); //old calc lacked a clamp, could potentially become negative damage - } //clamps laser damage to not go negative, allow some small amount of bleedthrough - ~30 Be/Steel will negate ABL, ~62 Ti, 42 DU else { - damage = initialDamage; - if (!pulseLaser) + HitpointTracker armor = p.GetComponent(); + if (laserDamage > 0) { - damage = initialDamage * TimeWarp.fixedDeltaTime; - } - } - p.ReduceArmor(damage / 10000); //really should be tied into diffuisvity, density, and SafeUseTemp - lasers would need to melt/ablate material away; needs to be in cm^3. Review later - p.AddDamage(damage); - if (pulseLaser) BattleDamageHandler.CheckDamageFX(p, caliber, 1 + (damage / initialDamage), HEpulses, false, part.vessel.GetName(), hit, false, false); //beams will proc BD once every scoreAccumulatorTick + var angularSpread = tanAngle * distance; //Scales down the damage based on the increased surface area of the area being hit by the laser. Think flashlight on a wall. + initialDamage = (laserDamage / (1 + Mathf.PI * angularSpread * angularSpread) * 0.425f); - } - if (HEpulses) - { - ExplosionFx.CreateExplosion(hit.point, - (laserDamage / 30000), - explModelPath, explSoundPath, ExplosionSourceType.Bullet, 1, null, vessel.vesselName, null); - } - if (HeatRay) - { - using (var hitsEnu2 = Physics.OverlapSphere(hit.point, (Mathf.Sin(maxDeviation) * (tf.position - laserPoint).magnitude), layerMask2).AsEnumerable().GetEnumerator()) - { - while (hitsEnu2.MoveNext()) - { - KerbalEVA kerb = hitsEnu2.Current.gameObject.GetComponentUpwards(); - Part hitP = kerb ? kerb.part : hitsEnu2.Current.GetComponentInParent(); - if (hitP == null) continue; - if (ProjectileUtils.IsIgnoredPart(hitP)) continue; - if (hitP && hitP != p && hitP.vessel && hitP.vessel != vessel) + if (armor != null)// technically, lasers shouldn't do damage until armor gone, but that would require localized armor tracking instead of the monolithic model currently used + { + damage = (initialDamage * (pulseLaser ? 1 : TimeWarp.fixedDeltaTime)) * Mathf.Clamp((1 - (BDAMath.Sqrt(armor.Diffusivity * (armor.Density / 1000)) * armor.ArmorThickness) / initialDamage), 0.005f, 1); //old calc lacked a clamp, could potentially become negative damage + } //clamps laser damage to not go negative, allow some small amount of bleedthrough - ~30 Be/Steel will negate ABL, ~62 Ti, 42 DU + else { - //p.AddDamage(damage); - p.AddSkinThermalFlux(damage); + damage = initialDamage; + if (!pulseLaser) + { + damage = initialDamage * TimeWarp.fixedDeltaTime; + } } + p.ReduceArmor(damage / 10000); //really should be tied into diffuisvity, density, and SafeUseTemp - lasers would need to melt/ablate material away; needs to be in cm^3. Review later + p.AddDamage(damage); + if (BDArmorySettings.DEBUG_WEAPONS) Debug.Log($"[BDArmory.ModuleWeapon]: Damage Applied to {p.name} on {p.vessel.GetName()}: {damage}"); + if (pulseLaser) BattleDamageHandler.CheckDamageFX(p, caliber, 1 + (damage / initialDamage), HEpulses, false, part.vessel.GetName(), hit, false, false); //beams will proc BD once every scoreAccumulatorTick } - } - } - if (graviticWeapon) - { - if (p.rb != null && p.rb.mass > 0) - { - float duration = BDArmorySettings.WEAPON_FX_DURATION; - if (!pulseLaser) + if (HEpulses) { - duration = BDArmorySettings.WEAPON_FX_DURATION * TimeWarp.fixedDeltaTime; + ExplosionFx.CreateExplosion(hit.point, + (laserDamage / 10000), + explModelPath, explSoundPath, ExplosionSourceType.Bullet, 1, null, vessel.vesselName, null); } - var ME = p.FindModuleImplementing(); - if (ME == null) + if (Impulse != 0) { - ME = (ModuleMassAdjust)p.AddModule("ModuleMassAdjust"); + if (!pulseLaser) + { + Impulse *= TimeWarp.fixedDeltaTime; + } + if (p.rb != null && p.rb.mass > 0) + { + //if (Impulse > 0) + //{ + p.rb.AddForceAtPosition((p.transform.position - tf.position).normalized * (float)Impulse, p.transform.position, ForceMode.Impulse); + //} + //else + //{ + // p.rb.AddForceAtPosition((tf.position - p.transform.position).normalized * (float)Impulse, p.transform.position, ForceMode.Impulse); + //} + if (BDArmorySettings.DEBUG_WEAPONS) Debug.Log($"[BDArmory.ModuleWeapon]: Impulse of {Impulse} Applied to {p.vessel.GetName()}"); + //if (laserDamage == 0) + damage += Impulse / 100; + } + } + if (graviticWeapon) + { + if (p.rb != null && p.rb.mass > 0) + { + float duration = BDArmorySettings.WEAPON_FX_DURATION; + if (!pulseLaser) + { + duration = BDArmorySettings.WEAPON_FX_DURATION * TimeWarp.fixedDeltaTime; + } + var ME = p.FindModuleImplementing(); + if (ME == null) + { + ME = (ModuleMassAdjust)p.AddModule("ModuleMassAdjust"); + } + ME.massMod += (massAdjustment * TimeWarp.fixedDeltaTime); + ME.duration += duration; + if (BDArmorySettings.DEBUG_WEAPONS) Debug.Log($"[BDArmory.ModuleWeapon]: Gravitic Buildup Applied to {p.vessel.GetName()}: {massAdjustment}t added"); + //if (laserDamage == 0) + damage += massAdjustment * 100; + } } - ME.massMod += (massAdjustment * TimeWarp.fixedDeltaTime); - ME.duration += duration; } } - if (instagib) - { - p.AddInstagibDamage(); - ExplosionFx.CreateExplosion(hit.point, - (1), "BDArmory/Models/explosion/explosion", explSoundPath, ExplosionSourceType.Bullet, 0, null, vessel.vesselName, null); - } var aName = vesselname; var tName = p.vessel.GetName(); if (BDACompetitionMode.Instance.Scores.RegisterBulletDamage(aName, tName, damage)) @@ -2236,8 +2567,13 @@ private void LaserBeam(string vesselname) { ScoreAccumulator = 0; BDACompetitionMode.Instance.Scores.RegisterBulletHit(aName, tName, WeaponName, distance); - if (!pulseLaser) BattleDamageHandler.CheckDamageFX(p, caliber, 1 + (damage / initialDamage), HEpulses, false, part.vessel.GetName(), hit, false, false); + if (!pulseLaser && laserDamage > 0) BattleDamageHandler.CheckDamageFX(p, caliber, 1 + (damage / initialDamage), HEpulses, false, part.vessel.GetName(), hit, false, false); //pulse lasers check battle damage earlier in the code + if (ProjectileUtils.isReportingWeapon(part) && BDACompetitionMode.Instance.competitionIsActive) + { + string message = $"{tName} hit by {aName}'s {OriginalShortName} at {distance:F3}m!"; + BDACompetitionMode.Instance.competitionStatus.Add(message); + } } else { @@ -2254,7 +2590,10 @@ private void LaserBeam(string vesselname) } else { - laserPoint = lr.transform.InverseTransformPoint((targetDirectionLR * maxTargetingRange) + tf.position); + if (isAPS && !pulseLaser) + laserPoint = (tgtShell != null ? tgtShell.transform.position : (tgtRocket != null ? tgtRocket.transform.position : lr.transform.InverseTransformPoint((targetDirectionLR * maxTargetingRange) + tf.position))); + else + laserPoint = lr.transform.InverseTransformPoint((targetDirectionLR * maxTargetingRange) + tf.position); lr.SetPosition(1, laserPoint); } } @@ -2271,7 +2610,7 @@ private void LaserBeam(string vesselname) } public void SetupLaserSpecifics() { - chargeSound = GameDatabase.Instance.GetAudioClip(chargeSoundPath); + //chargeSound = SoundUtils.GetAudioClip(chargeSoundPath); if (HighLogic.LoadedSceneIsFlight) { audioSource.clip = fireSound; @@ -2284,7 +2623,7 @@ public void SetupLaserSpecifics() { Transform tf = fireTransforms[i]; laserRenderers[i] = tf.gameObject.AddOrGetComponent(); - Color laserColor = Utils.ParseColor255(projectileColor); + Color laserColor = GUIUtils.ParseColor255(projectileColor); laserColor.a = laserColor.a / 2; laserRenderers[i].material = new Material(Shader.Find("KSP/Particles/Alpha Blended")); laserRenderers[i].material.SetColor("_TintColor", laserColor); @@ -2345,11 +2684,11 @@ public void FireRocket() //#11, #673 if (!rocketPod) timeGap *= fireTransforms.Length; - if (useRippleFire) + if (useRippleFire && fireState.Length > 1) { timeGap /= fireTransforms.Length; //to maintain RPM if only firing one barrel at a time } - if (Time.time - timeFired > timeGap && !isReloading || !pointingAtSelf && (aiControlled || !Utils.CheckMouseIsOnGui()) && WMgrAuthorized()) + if (Time.time - timeFired > timeGap && !isReloading || !pointingAtSelf && (aiControlled || !GUIUtils.CheckMouseIsOnGui()) && WMgrAuthorized()) {// fixes rocket ripple code for proper rippling bool effectsShot = false; for (float iTime = Mathf.Min(Time.time - timeFired - timeGap, TimeWarp.fixedDeltaTime); iTime >= 0; iTime -= timeGap) @@ -2388,18 +2727,26 @@ public void FireRocket() //#11, #673 rocket.explSoundPath = explSoundPath; rocket.spawnTransform = currentRocketTfm; rocket.caliber = rocketInfo.caliber; + rocket.apMod = rocketInfo.apMod; rocket.rocketMass = rocketMass; rocket.blastRadius = blastRadius; rocket.thrust = thrust; rocket.thrustTime = thrustTime; + rocket.lifeTime = rocketInfo.lifeTime; rocket.flak = proximityDetonation; rocket.detonationRange = detonationRange; rocket.maxAirDetonationRange = maxAirDetonationRange; rocket.tntMass = rocketInfo.tntMass; rocket.shaped = rocketInfo.shaped; - rocket.concussion = impulseWeapon; - rocket.gravitic = graviticWeapon; + rocket.concussion = rocketInfo.impulse; + rocket.gravitic = rocketInfo.gravitic; ; rocket.EMP = electroLaser; //borrowing this as a EMP weapon bool, since a rocket isn't going to be a laser + rocket.nuclear = rocketInfo.nuclear; + rocket.beehive = beehive; + if (beehive) + { + rocket.subMunitionType = rocketInfo.subMunitionType; + } rocket.choker = choker; rocket.impulse = Impulse; rocket.massMod = massAdjustment; @@ -2407,6 +2754,7 @@ public void FireRocket() //#11, #673 rocket.randomThrustDeviation = thrustDeviation; rocket.bulletDmgMult = bulletDmgMult; rocket.sourceVessel = vessel; + rocket.sourceWeapon = this.part; rocketObj.transform.SetParent(currentRocketTfm.parent); rocket.rocketName = GetShortName() + " rocket"; rocket.team = weaponManager.Team.Name; @@ -2421,6 +2769,7 @@ public void FireRocket() //#11, #673 rocket.isAPSprojectile = true; rocket.tgtShell = tgtShell; rocket.tgtRocket = tgtRocket; + if (delayTime > 0) rocket.lifeTime = delayTime; } rocketObj.SetActive(true); } @@ -2466,10 +2815,12 @@ public void FireRocket() //#11, #673 rocket.explSoundPath = explSoundPath; rocket.spawnTransform = currentRocketTfm; rocket.caliber = rocketInfo.caliber; + rocket.apMod = rocketInfo.apMod; rocket.rocketMass = rocketMass; rocket.blastRadius = blastRadius; rocket.thrust = thrust; rocket.thrustTime = thrustTime; + rocket.lifeTime = rocketInfo.lifeTime; rocket.flak = proximityDetonation; rocket.detonationRange = detonationRange; rocket.maxAirDetonationRange = maxAirDetonationRange; @@ -2478,6 +2829,12 @@ public void FireRocket() //#11, #673 rocket.concussion = impulseWeapon; rocket.gravitic = graviticWeapon; rocket.EMP = electroLaser; + rocket.nuclear = rocketInfo.nuclear; + rocket.beehive = beehive; + if (beehive) + { + rocket.subMunitionType = rocketInfo.subMunitionType; + } rocket.choker = choker; rocket.impulse = Impulse; rocket.massMod = massAdjustment; @@ -2485,6 +2842,7 @@ public void FireRocket() //#11, #673 rocket.randomThrustDeviation = thrustDeviation; rocket.bulletDmgMult = bulletDmgMult; rocket.sourceVessel = vessel; + rocket.sourceWeapon = this.part; rocketObj.transform.SetParent(currentRocketTfm); rocket.parentRB = part.rb; rocket.rocket = RocketInfo.rockets[currentType]; @@ -2499,6 +2857,7 @@ public void FireRocket() //#11, #673 rocket.isAPSprojectile = true; rocket.tgtShell = tgtShell; rocket.tgtRocket = tgtRocket; + if (delayTime > 0) rocket.lifeTime = delayTime; } rocketObj.SetActive(true); } @@ -2527,30 +2886,29 @@ public void FireRocket() //#11, #673 timeFired = Time.time - iTime; } } - if (useRippleFire) + if (fireState.Length > 1) { - if (fireState.Length > 1) //need to add clause for singlebarrel guns + barrelIndex++; + animIndex++; + //Debug.Log("[BDArmory.ModuleWeapon]: barrelIndex for " + this.GetShortName() + " is " + barrelIndex + "; total barrels " + fireTransforms.Length); + if ((!BurstFire || (BurstFire && (RoundsRemaining >= RoundsPerMag))) && barrelIndex + 1 > fireTransforms.Length) //only advance ripple index if weapon isn't brustfire, has finished burst, or has fired with all barrels { - barrelIndex++; - //Debug.Log("[BDArmory.ModuleWeapon]: barrelIndex for " + this.GetShortName() + " is " + barrelIndex + "; total barrels " + fireTransforms.Length); - if ((!BurstFire || (BurstFire && (RoundsRemaining >= RoundsPerMag))) && barrelIndex + 1 > fireTransforms.Length) //only advance ripple index if weapon isn't brustfire, has finished burst, or has fired with all barrels + StartCoroutine(IncrementRippleIndex(InitialFireDelay * TimeWarp.CurrentRate)); + isRippleFiring = true; + if (barrelIndex >= fireTransforms.Length) { - StartCoroutine(IncrementRippleIndex(initialFireDelay * TimeWarp.CurrentRate)); - isRippleFiring = true; - if (barrelIndex + 1 > fireTransforms.Length) - { - barrelIndex = 0; - //Debug.Log("[BDArmory.ModuleWeapon]: barrelIndex for " + this.GetShortName() + " reset"); - } + barrelIndex = 0; + //Debug.Log("[BDArmory.ModuleWeapon]: barrelIndex for " + this.GetShortName() + " reset"); } } - else + if (animIndex >= fireState.Length) animIndex = 0; + } + else + { + if (!BurstFire || (BurstFire && (RoundsRemaining >= RoundsPerMag))) { - if (!BurstFire || (BurstFire && (RoundsRemaining >= RoundsPerMag))) - { - StartCoroutine(IncrementRippleIndex(initialFireDelay * TimeWarp.CurrentRate)); - isRippleFiring = true; - } + StartCoroutine(IncrementRippleIndex(InitialFireDelay * TimeWarp.CurrentRate)); + isRippleFiring = true; } } } @@ -2630,10 +2988,15 @@ bool CanFire(float AmmoPerShot) if (BDArmorySettings.INFINITE_AMMO) return true; if (ECPerShot != 0) { - double chargeAvailable = part.RequestResource("ElectricCharge", ECPerShot, ResourceFlowMode.ALL_VESSEL); - if (chargeAvailable < ECPerShot * 0.95f && !CheatOptions.InfiniteElectricity) + vessel.GetConnectedResourceTotals(ECID, out double EcCurrent, out double ecMax); + if (EcCurrent > ECPerShot * 0.95f || CheatOptions.InfiniteElectricity) + { + part.RequestResource(ECID, ECPerShot, ResourceFlowMode.ALL_VESSEL); + if (requestResourceAmount == 0) return true; //weapon only uses ECperShot (electrolasers, mainly) + } + else { - ScreenMessages.PostScreenMessage("Weapon Requires EC", 5.0f, ScreenMessageStyle.UPPER_CENTER); + if (this.part.vessel.isActiveVessel) ScreenMessages.PostScreenMessage("Weapon Requires EC", 5.0f, ScreenMessageStyle.UPPER_CENTER); return false; } //else return true; //this is causing weapons thath have ECPerShot + standard ammo (railguns, etc) to not consume ammo, only EC @@ -2642,8 +3005,8 @@ bool CanFire(float AmmoPerShot) { return true; } - StartCoroutine(IncrementRippleIndex(useRippleFire ? initialFireDelay * TimeWarp.CurrentRate : 0)); //if out of ammo (howitzers, say, or other weapon with internal ammo, move on to next weapon; maybe it still has ammo - isRippleFiring = true; + StartCoroutine(IncrementRippleIndex(useRippleFire ? InitialFireDelay * TimeWarp.CurrentRate : 0)); //if out of ammo (howitzers, say, or other weapon with internal ammo, move on to next weapon; maybe it still has ammo + isRippleFiring = true; return false; } @@ -2652,15 +3015,12 @@ void PlayFireAnim() //Debug.Log("[BDArmory.ModuleWeapon]: fireState length = " + fireState.Length); for (int i = 0; i < fireState.Length; i++) { - try - { - //Debug.Log("[BDArmory.ModuleWeapon]: playing Fire Anim, i = " + i + "; fire anim " + fireState[i].name); - } - catch - { - Debug.Log("[BDArmory.ModuleWeapon]: error with fireanim number " + barrelIndex); - } - if ((!useRippleFire) || (useRippleFire && i == barrelIndex)) + // try { } + // catch + // { + // Debug.Log("[BDArmory.ModuleWeapon]: error with fireanim number " + barrelIndex); + // } + if ((!useRippleFire && fireTransforms.Length > 1) || (i == animIndex)) //play fireanims sequentially, unless a multibarrel weapon in salvomode, then play all fireanims simultaneously { float unclampedSpeed = (roundsPerMinute * fireState[i].length) / 60f; if (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 41) @@ -2679,6 +3039,7 @@ void PlayFireAnim() } fireState[i].speed = fireAnimSpeed; fireState[i].normalizedTime = Mathf.Repeat(fireState[i].normalizedTime, 1); + if (BDArmorySettings.DEBUG_WEAPONS) Debug.Log("[BDArmory.ModuleWeapon]: playing Fire Anim, i = " + i + "; fire anim " + fireState[i].name); } } } @@ -2686,6 +3047,10 @@ void PlayFireAnim() void WeaponFX() { //sound + if (ChargeTime > 0) + { + audioSource.Stop(); + } if (oneShotSound) { audioSource.Stop(); @@ -2696,8 +3061,9 @@ void WeaponFX() wasFiring = true; if (!audioSource.isPlaying) { + if (audioSource2.isPlaying) audioSource2.Stop(); // Stop any continuing cool-down sounds. audioSource.clip = fireSound; - audioSource.loop = false; + audioSource.loop = (soundRepeatTime == 0); audioSource.time = 0; audioSource.Play(); } @@ -2795,14 +3161,14 @@ void CheckWeaponSafety() // While I'm not saying vessels larger than 500m are impossible, let's be practical here const float maxCheckRange = 500f; - float checkRange = Mathf.Min(targetAcquired ? targetDistance : maxTargetingRange, maxCheckRange); + pointingDistance = Mathf.Min(targetAcquired ? targetDistance : maxTargetingRange, maxCheckRange); for (int i = 0; i < fireTransforms.Length; i++) { Ray ray = new Ray(fireTransforms[i].position, fireTransforms[i].forward); RaycastHit hit; - if (Physics.Raycast(ray, out hit, maxTargetingRange, layerMask1)) + if (Physics.Raycast(ray, out hit, pointingDistance, layerMask1)) { KerbalEVA eva = hit.collider.gameObject.GetComponentUpwards(); Part p = eva ? eva.part : hit.collider.gameObject.GetComponentInParent(); @@ -2813,24 +3179,34 @@ void CheckWeaponSafety() } } - pointingAtPosition = fireTransforms[i].position + (ray.direction * targetDistance); + pointingAtPosition = fireTransforms[i].position + (ray.direction * pointingDistance); } } HashSet enabledStates = new HashSet { WeaponStates.Enabled, WeaponStates.PoweringUp, WeaponStates.Locked }; - public void EnableWeapon() + public void EnableWeapon(bool secondaryFiring = false) { - if (enabledStates.Contains(weaponState)) + if (enabledStates.Contains(weaponState) || (secondaryFiring && weaponState == WeaponStates.EnabledForSecondaryFiring)) return; StopShutdownStartupRoutines(); - startupRoutine = StartCoroutine(StartupRoutine()); + startupRoutine = StartCoroutine(StartupRoutine(secondaryFiring: secondaryFiring)); } HashSet disabledStates = new HashSet { WeaponStates.Disabled, WeaponStates.PoweringDown }; public void DisableWeapon() { + if (dualModeAPS) isAPS = true; + if (isAPS) + { + if (ammoCount > 0 || !BDArmorySettings.INFINITE_AMMO) + { + //EnableWeapon(); + aiControlled = true; + return; + } + } if (disabledStates.Contains(weaponState)) return; @@ -2842,6 +3218,16 @@ public void DisableWeapon() HashSet standbyStates = new HashSet { WeaponStates.Standby, WeaponStates.PoweringUp, WeaponStates.Locked }; public void StandbyWeapon() { + if (dualModeAPS) isAPS = true; + if (isAPS) + { + if (ammoCount > 0 || !BDArmorySettings.INFINITE_AMMO) + { + //EnableWeapon(); + aiControlled = true; + return; + } + } if (standbyStates.Contains(weaponState)) return; if (disabledStates.Contains(weaponState)) @@ -2901,8 +3287,8 @@ void UpdateVolume() void SetupAudio() { - fireSound = GameDatabase.Instance.GetAudioClip(fireSoundPath); - overheatSound = GameDatabase.Instance.GetAudioClip(overheatSoundPath); + fireSound = SoundUtils.GetAudioClip(fireSoundPath); + overheatSound = SoundUtils.GetAudioClip(overheatSoundPath); if (!audioSource) { audioSource = gameObject.AddComponent(); @@ -2927,11 +3313,11 @@ void SetupAudio() if (reloadAudioPath != string.Empty) { - reloadAudioClip = (AudioClip)GameDatabase.Instance.GetAudioClip(reloadAudioPath); + reloadAudioClip = SoundUtils.GetAudioClip(reloadAudioPath); } if (reloadCompletePath != string.Empty) { - reloadCompleteAudioClip = (AudioClip)GameDatabase.Instance.GetAudioClip(reloadCompletePath); + reloadCompleteAudioClip = SoundUtils.GetAudioClip(reloadCompletePath); } if (!lowpassFilter && gameObject.GetComponents().Length == 0) @@ -2947,11 +3333,10 @@ void SetupAudio() #endregion Audio #region Targeting - void Aim() { //AI control - if (aiControlled && !slaved) + if (aiControlled && !slaved && !GPSTarget) { if (BDArmorySettings.RUNWAY_PROJECT && BDArmorySettings.RUNWAY_PROJECT_ROUND == 41) { @@ -2972,20 +3357,25 @@ void Aim() } Vector3 finalTarget = targetPosition; - if (aiControlled && !slaved && !targetAcquired && weaponManager) + bool manualAiming = false; + if (aiControlled && !slaved && weaponManager != null && (!targetAcquired || weaponManager.staleTarget)) { - if (!FloatingOrigin.Offset.IsZero() || !Krakensbane.GetFrameVelocity().IsZero()) + if (BDKrakensbane.IsActive) { - lastFinalAimTarget -= FloatingOrigin.OffsetNonKrakensbane; -#if DEBUG - debugLastTargetPosition -= FloatingOrigin.OffsetNonKrakensbane; -#endif + lastFinalAimTarget -= BDKrakensbane.FloatingOriginOffsetNonKrakensbane; + if (BDArmorySettings.DEBUG_LINES && BDArmorySettings.DEBUG_WEAPONS) + { + debugLastTargetPosition -= BDKrakensbane.FloatingOriginOffsetNonKrakensbane; + } } // Continue aiming towards where the target is expected to be while reloading based on the last measured pos, vel, acc. finalAimTarget = AIUtils.PredictPosition(lastFinalAimTarget, targetVelocity, targetAcceleration, Time.time - lastGoodTargetTime); // FIXME Check this predicted position when in orbit. -#if DEBUG - debugTargetPosition = AIUtils.PredictPosition(debugLastTargetPosition, targetVelocity, targetAcceleration, Time.time - lastGoodTargetTime); -#endif + fixedLeadOffset = targetPosition - finalAimTarget; //for aiming fixed guns to moving target + + if (BDArmorySettings.DEBUG_LINES && BDArmorySettings.DEBUG_WEAPONS) + { + debugTargetPosition = AIUtils.PredictPosition(debugLastTargetPosition, targetVelocity, targetAcceleration, Time.time - lastGoodTargetTime); + } } else { @@ -2994,48 +3384,63 @@ void Aim() { fireTransform = rockets[0].parent; // support for legacy RLs } - if (!slaved && !aiControlled && (yawRange > 0 || maxPitch - minPitch > 0) && !isAPS) + if (!slaved && !GPSTarget && !aiControlled && !MouseAimFlight.IsMouseAimActive && !isAPS && (vessel.isActiveVessel || BDArmorySettings.REMOTE_SHOOTING)) { - //MouseControl - Vector3 mouseAim = new Vector3(Input.mousePosition.x / Screen.width, Input.mousePosition.y / Screen.height, 0); - Ray ray = FlightCamera.fetch.mainCamera.ViewportPointToRay(mouseAim); - RaycastHit hit; - - if (Physics.Raycast(ray, out hit, maxTargetingRange, layerMask1)) + manualAiming = true; + targetVelocity = -BDKrakensbane.FrameVelocityV3f; // Stationary targets are being moved opposite to the Krakensbane frame velocity. + targetAcceleration = Vector3.zero; + if (yawRange > 0 || maxPitch - minPitch > 0) { - KerbalEVA eva = hit.collider.gameObject.GetComponentUpwards(); - Part p = eva ? eva.part : hit.collider.gameObject.GetComponentInParent(); + //MouseControl + Vector3 mouseAim = new Vector3(Input.mousePosition.x / Screen.width, Input.mousePosition.y / Screen.height, 0); + Ray ray = FlightCamera.fetch.mainCamera.ViewportPointToRay(mouseAim); + RaycastHit hit; - if (p != null && p.vessel != null && p.vessel == vessel) //aim through self vessel if occluding mouseray - { - targetPosition = ray.origin + ray.direction * maxTargetingRange; - } - else + if (Physics.Raycast(ray, out hit, maxTargetingRange, layerMask1)) { - targetPosition = hit.point; - } - targetAccelerationWasReset = true; - } - else - { - targetPosition = ray.origin + ray.direction * (maxTargetingRange + (FlightCamera.fetch.Distance * 0.75f)); + KerbalEVA eva = hit.collider.gameObject.GetComponentUpwards(); + Part p = eva ? eva.part : hit.collider.gameObject.GetComponentInParent(); - if (visualTargetVessel != null && visualTargetVessel.loaded) - { - if (!targetCOM && visualTargetPart != null) + if (p != null && p.vessel != null && p.vessel == vessel) //aim through self vessel if occluding mouseray { - targetPosition = ray.origin + ray.direction * Vector3.Distance(visualTargetPart.transform.position, ray.origin); + targetPosition = ray.origin + ray.direction * maxTargetingRange; } else { - targetPosition = ray.origin + ray.direction * Vector3.Distance(visualTargetVessel.transform.position, ray.origin); + targetPosition = hit.point; + } + if (p != null && p.rb != null && p.vessel != null) + { + targetVelocity = p.rb.velocity; + targetAcceleration = p.vessel.acceleration; } } else { - targetPosition = ray.origin + ray.direction * maxTargetingRange; + if (visualTargetVessel != null && visualTargetVessel.loaded) + { + if (!targetCOM && visualTargetPart != null) + { + targetPosition = ray.origin + ray.direction * Vector3.Distance(visualTargetPart.transform.position, ray.origin); + targetVelocity = visualTargetPart.rb.velocity; + targetAcceleration = visualTargetPart.vessel.acceleration; + } + else + { + targetPosition = ray.origin + ray.direction * Vector3.Distance(visualTargetVessel.transform.position, ray.origin); + targetVelocity = visualTargetVessel.rb_velocity; + targetAcceleration = visualTargetVessel.acceleration; + } + } + else + { + targetPosition = ray.origin + ray.direction * maxTargetingRange; + } } } + else if (!targetAcquired && (weaponManager == null || !weaponManager.staleTarget)) + targetPosition = fireTransform.position + fireTransform.forward * maxTargetingRange; // For fixed weapons, aim straight ahead (needed for targetDistance below for the trajectory sim) if no current target. + finalTarget = targetPosition; // In case aim assist and AI control is off. } if (BDArmorySettings.BULLET_WATER_DRAG) { @@ -3049,48 +3454,64 @@ void Aim() } //aim assist Vector3 originalTarget = targetPosition; + if (!manualAiming) targetPosition = AIUtils.PredictPosition(targetPosition, targetVelocity, targetAcceleration, Time.fixedDeltaTime); // Correct for the FI, which hasn't run yet, but does before visuals are next shown. targetDistance = Vector3.Distance(targetPosition, fireTransform.parent.position); origTargetDistance = targetDistance; if ((BDArmorySettings.AIM_ASSIST || aiControlled) && eWeaponType == WeaponTypes.Ballistic)//Gun targeting { - var gravity = (Vector3)FlightGlobals.getGeeForceAtPosition((fireTransforms[0].position + targetPosition) / 2f); - var bulletAcceleration = bulletDrop ? gravity : Vector3.zero; // Drag is ignored. - var bulletRelativePosition = targetPosition - fireTransforms[0].position; - var firingDirection = fireTransforms[0].forward; - Vector3 bulletEffectiveVelocity, bulletRelativeVelocity, bulletRelativeAcceleration, targetPredictedPosition, bulletDropOffset, lastFiringDirection; - float timeToCPA; + if (BDArmorySettings.DEBUG_LINES && BDArmorySettings.DEBUG_WEAPONS) debugCorrection = Vector3.zero; + Vector3 bulletRelativePosition, bulletEffectiveVelocity, bulletRelativeVelocity, bulletAcceleration, bulletRelativeAcceleration, targetPredictedPosition, bulletDropOffset, firingDirection, lastFiringDirection; + firingDirection = fireTransforms[0].forward; + var firePosition = fireTransforms[0].position + (baseBulletVelocity * firingDirection) * Time.fixedDeltaTime; // Bullets are initially placed up to 1 frame ahead (iTime). Not offsetting by part vel gives the correct initial placement. + bulletRelativePosition = targetPosition - firePosition; + float timeToCPA = BDAMath.Sqrt(bulletRelativePosition.sqrMagnitude / (targetVelocity - (part.rb.velocity + baseBulletVelocity * firingDirection)).sqrMagnitude); // Rough initial estimate. + targetPredictedPosition = AIUtils.PredictPosition(targetPosition, targetVelocity, targetAcceleration, timeToCPA); var count = 0; + // For artillery, TimeToCPA needs to use the furthest time, not the closest. do { lastFiringDirection = firingDirection; bulletEffectiveVelocity = part.rb.velocity + baseBulletVelocity * firingDirection; + firePosition = fireTransforms[0].position + (baseBulletVelocity * firingDirection) * Time.fixedDeltaTime; // Bullets are initially placed up to 1 frame ahead (iTime). + bulletAcceleration = bulletDrop ? (Vector3)FlightGlobals.getGeeForceAtPosition((firePosition + targetPredictedPosition) / 2f) : Vector3.zero; // Drag is ignored. + bulletRelativePosition = targetPosition - firePosition; bulletRelativeVelocity = targetVelocity - bulletEffectiveVelocity; bulletRelativeAcceleration = targetAcceleration - bulletAcceleration; - timeToCPA = AIUtils.ClosestTimeToCPA(bulletRelativePosition, bulletRelativeVelocity, bulletRelativeAcceleration, maxTargetingRange / bulletEffectiveVelocity.magnitude); + timeToCPA = AIUtils.TimeToCPA(bulletRelativePosition, bulletRelativeVelocity, bulletRelativeAcceleration, maxTargetingRange / bulletEffectiveVelocity.magnitude); targetPredictedPosition = AIUtils.PredictPosition(targetPosition, targetVelocity, targetAcceleration, timeToCPA); + // bulletPredictedPosition = AIUtils.PredictPosition(firePosition, bulletEffectiveVelocity, bulletAcceleration, timeToCPA); bulletDropOffset = -0.5f * bulletAcceleration * timeToCPA * timeToCPA; finalTarget = targetPredictedPosition + bulletDropOffset - part.rb.velocity * timeToCPA; firingDirection = (finalTarget - fireTransforms[0].position).normalized; - } while (++count < 10 && Vector3.Angle(lastFiringDirection, firingDirection) > 1f); // 1° margin of error is sufficient to prevent premature firing - targetDistance = Vector3.Distance(finalTarget, fireTransforms[0].position); - if (bulletDrop && targetDistance > 10000)// The above calculation becomes inaccurate for distances over approximately 10km due to surface curvature (varying gravity direction), so we try to narrow it down with a simulation. + } while (++count < 10 && Vector3.Angle(lastFiringDirection, firingDirection) > 1f); // 1° margin of error is sufficient to prevent premature firing (usually) + targetDistance = Vector3.Distance(finalTarget, firePosition); + if (bulletDrop && timeToCPA * bulletAcceleration.magnitude > 100f) // The above calculation becomes inaccurate for distances over approximately 10km (on Kerbin) due to surface curvature (varying gravity direction), so we try to narrow it down with a simulation. { - var simulatedCPA = BallisticTrajectoryClosestApproachSimulation(fireTransforms[0].position, bulletEffectiveVelocity, targetPosition, targetVelocity, targetAcceleration, BDArmorySettings.BALLISTIC_TRAJECTORY_SIMULATION_MULTIPLIER * Time.fixedDeltaTime); - var correction = simulatedCPA - AIUtils.PredictPosition(fireTransforms[0].position, bulletEffectiveVelocity, bulletAcceleration, timeToCPA); + var simulatedCPA = BallisticTrajectoryClosestApproachSimulation(firePosition, bulletEffectiveVelocity, targetPosition, targetVelocity, targetAcceleration, BDArmorySettings.BALLISTIC_TRAJECTORY_SIMULATION_MULTIPLIER * Time.fixedDeltaTime); + var correction = simulatedCPA - AIUtils.PredictPosition(firePosition, bulletEffectiveVelocity, bulletAcceleration, timeToCPA); + correction += 2f * (part.rb.velocity - targetVelocity) * Time.fixedDeltaTime; // Not entirely sure why this correction is needed, but it is. finalTarget -= correction; - targetDistance = Vector3.Distance(finalTarget, fireTransforms[0].position); - } -#if DEBUG - // Debug.Log($"DEBUG {count} iterations for convergence in aiming loop"); - debugTargetPosition = targetPosition; - debugLastTargetPosition = debugTargetPosition; - debugRelVelAdj = (targetVelocity - part.rb.velocity) * timeToCPA; - debugAccAdj = 0.5f * targetAcceleration * timeToCPA * timeToCPA; - debugGravAdj = bulletDropOffset; - // var missDistance = AIUtils.PredictPosition(bulletRelativePosition, bulletRelativeVelocity, bulletRelativeAcceleration, timeToCPA); - // if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("DEBUG δt: " + timeToCPA + ", miss: " + missDistance + ", bullet drop: " + bulletDropOffset + ", final: " + finalTarget + ", target: " + targetPosition + ", " + targetVelocity + ", " + targetAcceleration + ", distance: " + targetDistance); -#endif + if (BDArmorySettings.DEBUG_LINES && BDArmorySettings.DEBUG_WEAPONS) + { + debugCorrection = correction; + debugSimCPA = simulatedCPA; + debugBulletPred = AIUtils.PredictPosition(firePosition, bulletEffectiveVelocity, bulletAcceleration, timeToCPA); + debugTargetPred = targetPredictedPosition; + } + targetDistance = Vector3.Distance(finalTarget, firePosition); + } + if (BDArmorySettings.DEBUG_LINES && BDArmorySettings.DEBUG_WEAPONS) + { + // Debug.Log($"DEBUG {count} iterations for convergence in aiming loop"); + debugTargetPosition = targetPosition; + debugLastTargetPosition = debugTargetPosition; + debugRelVelAdj = (targetVelocity - part.rb.velocity) * timeToCPA; + debugAccAdj = 0.5f * targetAcceleration * timeToCPA * timeToCPA; + debugGravAdj = bulletDropOffset; + // var missDistance = AIUtils.PredictPosition(bulletRelativePosition, bulletRelativeVelocity, bulletRelativeAcceleration, timeToCPA); + // if (BDArmorySettings.DEBUG_WEAPONS) Debug.Log("DEBUG δt: " + timeToCPA + ", miss: " + missDistance + ", bullet drop: " + bulletDropOffset + ", final: " + finalTarget + ", target: " + targetPosition + ", " + targetVelocity + ", " + targetAcceleration + ", distance: " + targetDistance); + } } if ((BDArmorySettings.AIM_ASSIST || aiControlled) && eWeaponType == WeaponTypes.Rocket) //Rocket targeting { @@ -3124,16 +3545,22 @@ void Aim() { turret.smoothRotation = false; } - turret.AimToTarget(finalAimTarget); + turret.AimToTarget(finalAimTarget); //no aimbot turrets when target out of sight turret.smoothRotation = origSmooth; } } - //moving RTS to get all the targeting code together for convenience once rockets get added + + /// + /// Run a trajectory simulation in the current frame. + /// + /// Note: Since this is running in the current frame, for moving targets the trajectory appears to be off, but it's not. + /// By the time the projectile arrives at the target, the target has moved to that point in the trajectory. + /// public void RunTrajectorySimulation() { if ((eWeaponType == WeaponTypes.Rocket && ((BDArmorySettings.AIM_ASSIST && BDArmorySettings.DRAW_AIMERS && vessel.isActiveVessel) || aiControlled)) || (BDArmorySettings.AIM_ASSIST && BDArmorySettings.DRAW_AIMERS && - (BDArmorySettings.DRAW_DEBUG_LINES || (vessel && vessel.isActiveVessel && !aiControlled && !MapView.MapIsEnabled && !pointingAtSelf && eWeaponType != WeaponTypes.Rocket)))) + (BDArmorySettings.DEBUG_LINES || (vessel && vessel.isActiveVessel && !aiControlled && !MapView.MapIsEnabled && !pointingAtSelf && eWeaponType != WeaponTypes.Rocket)))) { Transform fireTransform = fireTransforms[0]; @@ -3158,27 +3585,39 @@ public void RunTrajectorySimulation() } else if (eWeaponType == WeaponTypes.Ballistic && BDArmorySettings.AIM_ASSIST && BDArmorySettings.DRAW_AIMERS) { - Vector3 simVelocity = part.rb.velocity + (baseBulletVelocity * fireTransform.forward); - Vector3 simCurrPos = fireTransform.position; - var simDeltaTime = Mathf.Clamp(Mathf.Min(maxTargetingRange, Mathf.Max(targetDistance, origTargetDistance)) / simVelocity.magnitude / 2f, Time.fixedDeltaTime, Time.fixedDeltaTime * BDArmorySettings.BALLISTIC_TRAJECTORY_SIMULATION_MULTIPLIER); // With leap-frog, we can use a higher time-step and still get better accuracy than forward Euler (what was used before). Always take at least 2 steps though. - var timeOfFlight = BallisticTrajectorySimulation(ref simCurrPos, simVelocity, Mathf.Min(maxTargetingRange, targetDistance), maxTargetingRange / bulletVelocity, simDeltaTime, FlightGlobals.getAltitudeAtPos(targetPosition) < 0); - bulletPrediction = simCurrPos; + Vector3 simVelocity = (part.rb.velocity + BDKrakensbane.FrameVelocityV3f) + baseBulletVelocity * fireTransform.forward; + if (Physics.Raycast(new Ray(fireTransform.position, baseBulletVelocity * fireTransform.forward), out RaycastHit hit, Time.fixedDeltaTime * baseBulletVelocity, layerMask1)) // Check between the barrel and the point the bullet appears. + { + bulletPrediction = hit.point; + } + else + { + Vector3 simCurrPos = fireTransform.position + (baseBulletVelocity * fireTransform.forward) * Time.fixedDeltaTime; // Bullets are initially placed up to 1 frame ahead (iTime). + var simDeltaTime = Mathf.Clamp(Mathf.Min(maxTargetingRange, Mathf.Max(targetDistance, origTargetDistance)) / simVelocity.magnitude / 2f, Time.fixedDeltaTime, Time.fixedDeltaTime * BDArmorySettings.BALLISTIC_TRAJECTORY_SIMULATION_MULTIPLIER); // With leap-frog, we can use a higher time-step and still get better accuracy than forward Euler (what was used before). Always take at least 2 steps though. + var timeOfFlight = BallisticTrajectorySimulation(ref simCurrPos, simVelocity, Mathf.Min(maxTargetingRange, (simCurrPos - targetPosition).magnitude), maxTargetingRange / baseBulletVelocity, simDeltaTime, FlightGlobals.getAltitudeAtPos(targetPosition) < 0); + bulletPrediction = simCurrPos; + } Vector3 pointingPos = fireTransform.position + (fireTransform.forward * targetDistance); - trajectoryOffset = pointingPos - bulletPrediction; } else if (eWeaponType == WeaponTypes.Rocket) { float simTime = 0; + float maxTime = rocketInfo.lifeTime; + float maxDistance = Mathf.Min(Mathf.Min(targetDistance, maxTargetingRange), maxAirDetonationRange); // Rockets often detonate earlier than their lifetime. Vector3 pointingDirection = fireTransform.forward; - Vector3 simVelocity = part.rb.velocity + Krakensbane.GetFrameVelocityV3f(); + Vector3 simVelocity = part.rb.velocity + BDKrakensbane.FrameVelocityV3f; Vector3 simCurrPos = fireTransform.position; Vector3 simPrevPos = simCurrPos; Vector3 simStartPos = simCurrPos; + Vector3 closestPointOfApproach = simCurrPos; + float closestDistanceSqr = float.MaxValue; + RaycastHit hit; + bool hitDetected = false; float simDeltaTime = Time.fixedDeltaTime; float atmosMultiplier = Mathf.Clamp01(2.5f * (float)FlightGlobals.getAtmDensity(vessel.staticPressurekPa, vessel.externalTemperature, vessel.mainBody)); bool slaved = turret && weaponManager && (weaponManager.slavingTurrets || weaponManager.guardMode); - if (BDArmorySettings.DRAW_DEBUG_LINES && BDArmorySettings.DRAW_AIMERS) + if (BDArmorySettings.DEBUG_LINES && BDArmorySettings.DRAW_AIMERS) { if (trajectoryPoints == null) trajectoryPoints = new List(); trajectoryPoints.Clear(); @@ -3193,7 +3632,6 @@ public void RunTrajectorySimulation() while (true) { - RaycastHit hit; // No longer thrusting, finish up with a ballistic sim. if (simTime > thrustTime) @@ -3203,12 +3641,22 @@ public void RunTrajectorySimulation() { simVelocity -= 0.5f * simDeltaTime * gravity; } simVelocity -= 0.5f * thrust / rocketMass * simDeltaTime * pointingDirection; // Note: we're ignoring the underwater slow-down here. - var currentTargetDistance = Mathf.Min(maxTargetingRange, (simCurrPos - targetPosition).magnitude); - simDeltaTime = Mathf.Clamp(currentTargetDistance / simVelocity.magnitude / 2f, Time.fixedDeltaTime, Time.fixedDeltaTime * BDArmorySettings.BALLISTIC_TRAJECTORY_SIMULATION_MULTIPLIER); - var timeToCPA = AIUtils.ClosestTimeToCPA(targetPosition - simCurrPos, targetVelocity - simVelocity, targetAcceleration - gravity, maxTargetingRange / bulletVelocity - simTime); // For aiming, we want the closest approach to refine our aim. - bulletPrediction = AIUtils.PredictPosition(simCurrPos, simVelocity, gravity, timeToCPA); + var distanceRemaining = Mathf.Min(maxDistance - (simCurrPos - simStartPos).magnitude, (targetPosition - simCurrPos).magnitude); + var timeRemaining = maxTime - simTime; + simDeltaTime = Mathf.Clamp(Mathf.Min(distanceRemaining / simVelocity.magnitude, timeRemaining) / 8f, Time.fixedDeltaTime, Time.fixedDeltaTime * BDArmorySettings.BALLISTIC_TRAJECTORY_SIMULATION_MULTIPLIER); // Take 8 steps for smoother visuals. + var timeToCPA = AIUtils.TimeToCPA(targetPosition - simCurrPos, targetVelocity - simVelocity, targetAcceleration - gravity, timeRemaining); // For aiming, we want the closest approach to refine our aim. + closestPointOfApproach = AIUtils.PredictPosition(simCurrPos, simVelocity, gravity, timeToCPA); + if (!hitDetected) bulletPrediction = closestPointOfApproach; + if (BDArmorySettings.AIM_ASSIST && BDArmorySettings.DRAW_AIMERS && !hitDetected) + { + var timeOfFlight = BallisticTrajectorySimulation(ref simCurrPos, simVelocity, distanceRemaining, timeRemaining, simDeltaTime, FlightGlobals.getAltitudeAtPos(targetPosition) < 0, SimulationStage.Normal, false); // For visuals, we want the trajectory sim with collision detection. Note: this is done after to avoid messing with simCurrPos. + if (!hitDetected) + { + bulletPrediction = simCurrPos; // Overwrite the bulletPrediction with the results of the trajectory sim if a hit was detected. + hitDetected = true; + } + } simTime += timeToCPA; - if (BDArmorySettings.AIM_ASSIST && BDArmorySettings.DRAW_AIMERS) BallisticTrajectorySimulation(ref simCurrPos, simVelocity, currentTargetDistance, maxTargetingRange / bulletVelocity - simTime, simDeltaTime, FlightGlobals.getAltitudeAtPos(targetPosition) < 0, SimulationStage.Normal, false); // For visuals, we want the trajectory sim with collision detection. Note: this is done after to avoid messing with simCurrPos. break; } @@ -3216,86 +3664,52 @@ public void RunTrajectorySimulation() simTime += simDeltaTime; // Position update (current time). + simPrevPos = simCurrPos; simCurrPos += simVelocity * simDeltaTime; - if (BDArmorySettings.DRAW_DEBUG_LINES && BDArmorySettings.DRAW_AIMERS) - trajectoryPoints.Add(simCurrPos); - // Check for collisions. - if (!aiControlled && !slaved) + // Check for collisions within the last update. + if (!hitDetected && !aiControlled && !slaved) { - if (Physics.Raycast(simPrevPos, simCurrPos - simPrevPos, out hit, Vector3.Distance(simPrevPos, simCurrPos), layerMask1)) + if (Physics.Raycast(simPrevPos, simVelocity, out hit, Vector3.Distance(simPrevPos, simCurrPos), layerMask1) && (hit.collider != null && hit.collider.gameObject != null && hit.collider.gameObject.GetComponentInParent() != part)) // Any hit other than the part firing the rocket. { - /* - Vessel hitVessel = null; - try - { - if (hit.collider.gameObject != FlightGlobals.currentMainBody.gameObject) // Ignore terrain hits. FIXME The collider could still be a building (SpaceCenterBuilding?), but chances of this is low. - { - KerbalEVA eva = hit.collider.gameObject.GetComponentUpwards(); - var part = eva ? eva.part : hit.collider.gameObject.GetComponentInParent(); - if (part) - { - hitVessel = part.vessel; - } - } - } - catch (NullReferenceException e) - { - Debug.LogError("[BDArmory.ModuleWeapon]: NullReferenceException while simulating trajectory: " + e.Message); - } - - if (hitVessel == null || hitVessel != vessel) - { - bulletPrediction = hit.point; //this is why rocket aimers appear a few meters infront of muzzle - break; - } - */ - try - { - if (hit.collider.gameObject == FlightGlobals.currentMainBody.gameObject) - { - bulletPrediction = hit.point; - break; - } - else - { - KerbalEVA eva = hit.collider.gameObject.GetComponentUpwards(); - DestructibleBuilding building = hit.collider.gameObject.GetComponentUpwards(); - var part = eva ? eva.part : hit.collider.gameObject.GetComponentInParent(); - if (part || building != null) - { - bulletPrediction = hit.point; - break; - } - } - } - catch (NullReferenceException e) - { - Debug.LogError("[BDArmory.ModuleWeapon]: NullReferenceException while simulating trajectory: " + e.Message); - } + bulletPrediction = hit.point; + hitDetected = true; } - //else if (FlightGlobals.getAltitudeAtPos(simCurrPos) < 0) // Note: this prevents aiming below sea-level. - //{ + // else if (FlightGlobals.getAltitudeAtPos(simCurrPos) < 0) // Note: this prevents aiming below sea-level. + // { // bulletPrediction = simCurrPos; // break; - //} + // } } - // Book-keeping and max distance checks. - simPrevPos = simCurrPos; - if ((simStartPos - simCurrPos).sqrMagnitude > targetDistance * targetDistance) + // Check for closest approach within the last update. + if ((simPrevPos - targetPosition).sqrMagnitude < closestDistanceSqr) + { + var timeToCPA = AIUtils.TimeToCPA(targetPosition - simPrevPos, targetVelocity - simVelocity, targetAcceleration - gravity, simDeltaTime); + if (timeToCPA < simDeltaTime) + closestPointOfApproach = AIUtils.PredictPosition(simPrevPos, simVelocity, gravity, timeToCPA); + else + closestPointOfApproach = simPrevPos; + closestDistanceSqr = (closestPointOfApproach - targetPosition).sqrMagnitude; + } + else { - bulletPrediction = simStartPos + (simCurrPos - simStartPos).normalized * targetDistance; + if (!hitDetected) bulletPrediction = closestPointOfApproach; break; } - if ((simStartPos - simCurrPos).sqrMagnitude > maxTargetingRange * maxTargetingRange) + + if (BDArmorySettings.DEBUG_LINES && BDArmorySettings.DRAW_AIMERS && !hitDetected) + trajectoryPoints.Add(simCurrPos); + + // Book-keeping and max distance checks. + if (simTime > maxTime || (simStartPos - simCurrPos).sqrMagnitude > maxDistance * maxDistance) { - bulletPrediction = simStartPos + ((simCurrPos - simStartPos).normalized * maxTargetingRange); + if (!hitDetected) bulletPrediction = simCurrPos; break; } // Rotation (aero stabilize). - pointingDirection = Vector3.RotateTowards(pointingDirection, simVelocity + Krakensbane.GetFrameVelocityV3f(), atmosMultiplier * (0.5f * simTime) * 50 * simDeltaTime * Mathf.Deg2Rad, 0); + pointingDirection = Vector3.RotateTowards(pointingDirection, simVelocity + BDKrakensbane.FrameVelocityV3f, atmosMultiplier * (0.5f * simTime) * 50 * simDeltaTime * Mathf.Deg2Rad, 0); // Velocity update (half of current time and half of the next... that's why it's called leapfrog). if (simTime < thrustTime) @@ -3315,7 +3729,7 @@ public void RunTrajectorySimulation() } // Visuals - if (BDArmorySettings.DRAW_DEBUG_LINES && BDArmorySettings.DRAW_AIMERS) + if (BDArmorySettings.DEBUG_LINES && BDArmorySettings.DRAW_AIMERS) { trajectoryPoints.Add(bulletPrediction); trajectoryRenderer = gameObject.GetComponent(); @@ -3328,23 +3742,17 @@ public void RunTrajectorySimulation() trajectoryRenderer.enabled = true; trajectoryRenderer.positionCount = trajectoryPoints.Count; int i = 0; + var offset = BDKrakensbane.IsActive ? Vector3.zero : AIUtils.PredictPosition(Vector3.zero, vessel.Velocity(), vessel.acceleration, Time.fixedDeltaTime); using (var point = trajectoryPoints.GetEnumerator()) while (point.MoveNext()) { - trajectoryRenderer.SetPosition(i++, point.Current); + trajectoryRenderer.SetPosition(i, point.Current + offset); + ++i; } } - else - { - if (trajectoryRenderer != null) - { - trajectoryRenderer.enabled = false; - trajectoryRenderer = null; - } - } Vector3 pointingPos = fireTransform.position + (fireTransform.forward * targetDistance); - trajectoryOffset = pointingPos - bulletPrediction; + trajectoryOffset = pointingPos - closestPointOfApproach; predictedFlightTime = simTime; } } @@ -3365,68 +3773,99 @@ public float BallisticTrajectorySimulation(ref Vector3 position, Vector3 velocit { float elapsedTime = 0f; var startPosition = position; - var maxDistanceSqr = maxDistance * maxDistance; if (FlightGlobals.getAltitudeAtPos(position) < 0) ignoreWater = true; - var gravity = FlightGlobals.getGeeForceAtPosition(position); - velocity += 0.5 * timeStep * gravity; // Boot-strap velocity calculation. + var gravity = (Vector3)FlightGlobals.getGeeForceAtPosition(position); + velocity += 0.5f * timeStep * gravity; // Boot-strap velocity calculation. Ray ray = new Ray(); RaycastHit hit; - if (resetTrajectoryPoints && BDArmorySettings.DRAW_DEBUG_LINES && BDArmorySettings.DRAW_AIMERS) + if (BDArmorySettings.DEBUG_LINES && BDArmorySettings.DRAW_AIMERS) { if (trajectoryPoints == null) trajectoryPoints = new List(); - trajectoryPoints.Clear(); + if (resetTrajectoryPoints) + trajectoryPoints.Clear(); + if (trajectoryPoints.Count == 0) + trajectoryPoints.Add(fireTransforms[0].position); + trajectoryPoints.Add(position); } while (elapsedTime < maxTime) { ray.origin = position; ray.direction = velocity; - var altitude = FlightGlobals.getAltitudeAtPos(position + velocity * timeStep); - if ((Physics.Raycast(ray, out hit, timeStep * velocity.magnitude, layerMask1) && (hit.collider != null && hit.collider.gameObject != null && hit.collider.gameObject.GetComponentInParent() != part)) // Ignore the part firing the projectile. - || (!ignoreWater && altitude < 0)) + var deltaPosition = timeStep * velocity; + var deltaDistance = deltaPosition.magnitude; + var elapsedDistance = (startPosition - position).magnitude; + var altitude = FlightGlobals.getAltitudeAtPos(position + deltaPosition); + if ((Physics.Raycast(ray, out hit, deltaDistance, layerMask1) && (hit.collider != null && hit.collider.gameObject != null && hit.collider.gameObject.GetComponentInParent() != part)) // Ignore the part firing the projectile. + || (!ignoreWater && altitude < 0) // Underwater + || (stage == SimulationStage.Normal && elapsedTime + timeStep > maxTime) // Out of time + || (stage == SimulationStage.Normal && maxDistance - elapsedDistance < deltaDistance)) // Out of distance { switch (stage) { case SimulationStage.Normal: - if (BDArmorySettings.DRAW_DEBUG_LINES && BDArmorySettings.DRAW_AIMERS && trajectoryPoints.Count == 0) - trajectoryPoints.Add(position); - goto case SimulationStage.Refining; + { + if (elapsedTime + timeStep > maxTime) // Final time amount. + { + // Debug.Log($"DEBUG Refining trajectory sim due to final time, time: {elapsedTime}, {timeStep}, {maxTime}, dist: {maxDistance}, {elapsedDistance}, {deltaDistance}"); + velocity -= 0.5f * timeStep * gravity; // Correction to final velocity. + var finalTime = BallisticTrajectorySimulation(ref position, velocity, maxDistance - elapsedDistance, maxTime - elapsedTime, (maxTime - elapsedTime) / 4f, ignoreWater, SimulationStage.Final, false); + elapsedTime += finalTime; + } + else if (maxDistance - elapsedDistance < deltaDistance) // Final distance amount. + { + // Debug.Log($"DEBUG Refining trajectory sim due to final distance, time: {elapsedTime}, {timeStep}, {maxTime}, dist: {maxDistance}, {elapsedDistance}, {deltaDistance}"); + velocity -= 0.5f * timeStep * gravity; // Correction to final velocity. + var newTimeStep = timeStep * (maxDistance - elapsedDistance) / deltaDistance; + var finalTime = BallisticTrajectorySimulation(ref position, velocity, maxDistance - elapsedDistance, newTimeStep, newTimeStep / 4f, ignoreWater, SimulationStage.Final, false); + elapsedTime += finalTime; + } + else + goto case SimulationStage.Refining; + break; + } case SimulationStage.Refining: // Perform a more accurate final step for the collision. - velocity -= 0.5f * timeStep * gravity; // Correction to final velocity. - var finalTime = BallisticTrajectorySimulation(ref position, velocity, velocity.magnitude * timeStep, timeStep, timeStep / 4f, ignoreWater, timeStep > 5f * Time.fixedDeltaTime ? SimulationStage.Refining : SimulationStage.Final, false); - elapsedTime += finalTime; - break; - case SimulationStage.Final: - if (!ignoreWater && altitude < 0) { - var currentAltitude = FlightGlobals.getAltitudeAtPos(position); - timeStep *= currentAltitude / (currentAltitude - altitude); - elapsedTime += timeStep; - position += timeStep * velocity; - // Debug.Log("DEBUG breaking trajectory sim due to water at " + position.ToString("F6") + " at altitude " + FlightGlobals.getAltitudeAtPos(position)); + // Debug.Log($"DEBUG Refining trajectory sim, time: {elapsedTime}, {timeStep}, {maxTime}, dist: {maxDistance}, {elapsedDistance}, {deltaDistance}"); + velocity -= 0.5f * timeStep * gravity; // Correction to final velocity. + var finalTime = BallisticTrajectorySimulation(ref position, velocity, velocity.magnitude * timeStep, timeStep, timeStep / 4f, ignoreWater, timeStep > 5f * Time.fixedDeltaTime ? SimulationStage.Refining : SimulationStage.Final, false); + elapsedTime += finalTime; + break; } - else + case SimulationStage.Final: { - elapsedTime += (hit.point - position).magnitude / velocity.magnitude; - position = hit.point; - // Debug.Log("DEBUG breaking trajectory sim due to hit at " + position.ToString("F6") + " at altitude " + FlightGlobals.getAltitudeAtPos(position)); + if (!ignoreWater && altitude < 0) // Underwater + { + var currentAltitude = FlightGlobals.getAltitudeAtPos(position); + timeStep *= currentAltitude / (currentAltitude - altitude); + elapsedTime += timeStep; + position += timeStep * velocity; + // Debug.Log("DEBUG breaking trajectory sim due to water at " + position.ToString("F6") + " at altitude " + FlightGlobals.getAltitudeAtPos(position)); + } + else // Collision + { + elapsedTime += (hit.point - position).magnitude / velocity.magnitude; + position = hit.point; + // Debug.Log("DEBUG breaking trajectory sim due to hit at " + position.ToString("F6") + " at altitude " + FlightGlobals.getAltitudeAtPos(position)); + } + break; } - break; } break; } - if (BDArmorySettings.DRAW_DEBUG_LINES && BDArmorySettings.DRAW_AIMERS && stage != SimulationStage.Final) + if (BDArmorySettings.DEBUG_LINES && BDArmorySettings.DRAW_AIMERS && stage != SimulationStage.Final) trajectoryPoints.Add(position); - position += timeStep * velocity; - gravity = FlightGlobals.getGeeForceAtPosition(position); + position += deltaPosition; + gravity = (Vector3)FlightGlobals.getGeeForceAtPosition(position); velocity += timeStep * gravity; elapsedTime += timeStep; - if ((startPosition - position).sqrMagnitude > maxDistanceSqr) + if (elapsedDistance > maxDistance) { - // Debug.Log("DEBUG breaking trajectory sim due to max distance: " + maxDistance.ToString("F6") + " at altitude " + FlightGlobals.getAltitudeAtPos(position)); + // Debug.Log($"DEBUG breaking trajectory sim due to max distance: {maxDistance} at altitude {FlightGlobals.getAltitudeAtPos(position)}"); break; } } - if (BDArmorySettings.DRAW_DEBUG_LINES && BDArmorySettings.DRAW_AIMERS && stage == SimulationStage.Normal) + // if (elapsedTime > maxTime) Debug.Log($"DEBUG Time elapsed: {elapsedTime} / {maxTime}, dist: {maxDistance}, {(startPosition - position).magnitude}, {(timeStep * velocity).magnitude}"); + if (BDArmorySettings.DEBUG_LINES && BDArmorySettings.DRAW_AIMERS && resetTrajectoryPoints) { trajectoryPoints.Add(position); trajectoryRenderer = gameObject.GetComponent(); @@ -3435,15 +3874,16 @@ public float BallisticTrajectorySimulation(ref Vector3 position, Vector3 velocit trajectoryRenderer = gameObject.AddComponent(); trajectoryRenderer.startWidth = .1f; trajectoryRenderer.endWidth = .1f; - trajectoryPoints = new List(); } trajectoryRenderer.enabled = true; trajectoryRenderer.positionCount = trajectoryPoints.Count; int i = 0; + var offset = BDKrakensbane.IsActive ? Vector3.zero : AIUtils.PredictPosition(Vector3.zero, vessel.Velocity(), vessel.acceleration, Time.fixedDeltaTime); using (var point = trajectoryPoints.GetEnumerator()) while (point.MoveNext()) { - trajectoryRenderer.SetPosition(i++, point.Current); + trajectoryRenderer.SetPosition(i, point.Current + offset); + ++i; } } return elapsedTime; @@ -3457,6 +3897,8 @@ public float BallisticTrajectorySimulation(ref Vector3 position, Vector3 velocit /// /// /// + /// + /// /// /// The CPA to the target. public Vector3 BallisticTrajectoryClosestApproachSimulation(Vector3 position, Vector3 velocity, Vector3 targetPosition, Vector3 targetVelocity, Vector3 targetAcceleration, float timeStep, float elapsedTime = 0, SimulationStage stage = SimulationStage.Normal) @@ -3482,9 +3924,10 @@ public Vector3 BallisticTrajectoryClosestApproachSimulation(Vector3 position, Ve case SimulationStage.Refining: // Perform a more accurate final step for the collision. return BallisticTrajectoryClosestApproachSimulation(lastPosition, velocity - 0.5f * timeStep * gravity, targetPosition, targetVelocity, targetAcceleration, timeStep / 4f, elapsedTime, timeStep > 5f * Time.fixedDeltaTime ? SimulationStage.Refining : SimulationStage.Final); case SimulationStage.Final: - var timeToCPA = AIUtils.ClosestTimeToCPA(lastPosition - lastPredictedTargetPosition, velocity - (predictedTargetPosition - lastPredictedTargetPosition) / timeStep, Vector3.zero, timeStep); + var timeToCPA = AIUtils.TimeToCPA(lastPosition - lastPredictedTargetPosition, velocity - (predictedTargetPosition - lastPredictedTargetPosition) / timeStep, Vector3.zero, timeStep); position = lastPosition + timeToCPA * velocity; - predictedTargetPosition = AIUtils.PredictPosition(targetPosition, targetVelocity, targetAcceleration, elapsedTime + timeToCPA); + // elapsedTime += timeToCPA; + // predictedTargetPosition = AIUtils.PredictPosition(targetPosition, targetVelocity, targetAcceleration, elapsedTime); return position; } } @@ -3503,6 +3946,7 @@ public Vector3 GetLeadOffset() } public float targetCosAngle; + public bool safeToFire; void CheckAIAutofire() { //autofiring with AI @@ -3519,42 +3963,67 @@ void CheckAIAutofire() targetCosAngle = Vector3.Dot(aimDirection, targetRelPos.normalized); var maxAutoFireCosAngle2 = targetAdjustedMaxCosAngle; - if (eWeaponType != WeaponTypes.Rocket) //guns/lasers + safeToFire = CheckForFriendlies(fireTransform); + if (safeToFire) { - // Vector3 targetDiffVec = finalAimTarget - lastFinalAimTarget; - // Vector3 projectedTargetPos = targetDiffVec; - //projectedTargetPos /= TimeWarp.fixedDeltaTime; - //projectedTargetPos *= TimeWarp.fixedDeltaTime; - // projectedTargetPos *= 2; //project where the target will be in 2 timesteps - // projectedTargetPos += finalAimTarget; - - // targetDiffVec.Normalize(); - // Vector3 lastTargetRelPos = (lastFinalAimTarget) - fireTransform.position; - - if (BDATargetManager.CheckSafeToFireGuns(weaponManager, aimDirection, 1000, 0.999962f) //~0.5 degree of unsafe angle, was 0.999848f (1deg) - && targetCosAngle >= maxAutoFireCosAngle2) //check if directly on target + if (eWeaponType == WeaponTypes.Ballistic || eWeaponType == WeaponTypes.Laser) { - autoFire = true; + autoFire = (targetCosAngle >= targetAdjustedMaxCosAngle); } - else - { - autoFire = false; - } - } - else // rockets - { - if (BDATargetManager.CheckSafeToFireGuns(weaponManager, aimDirection, 1000, 0.999848f)) + else // Rockets + { autoFire = (targetCosAngle >= targetAdjustedMaxCosAngle) && ((finalAimTarget - fireTransform.position).sqrMagnitude > (blastRadius * blastRadius) * 2); } + + if (autoFire && Vector3.Angle(targetPosition - fireTransform.position, aimDirection) < 5) //check LoS for direct-fire weapons { - if ((Vector3.Distance(finalAimTarget, fireTransform.position) > blastRadius) && (targetCosAngle >= maxAutoFireCosAngle2)) - { - autoFire = true; //rockets already calculate where target will be - } - else + if (RadarUtils.TerrainCheck(targetPosition, fireTransform.position)) { autoFire = false; } } } + else + { + autoFire = false; + } + if (autoFire && weaponManager.staleTarget && (lastVisualTargetVessel != null && lastVisualTargetVessel.LandedOrSplashed && vessel.LandedOrSplashed)) autoFire = false; //ground Vee engaging another ground Vee which has ducked out of sight, don't fire + + // if (eWeaponType != WeaponTypes.Rocket) //guns/lasers + // { + // // Vector3 targetDiffVec = finalAimTarget - lastFinalAimTarget; + // // Vector3 projectedTargetPos = targetDiffVec; + // //projectedTargetPos /= TimeWarp.fixedDeltaTime; + // //projectedTargetPos *= TimeWarp.fixedDeltaTime; + // // projectedTargetPos *= 2; //project where the target will be in 2 timesteps + // // projectedTargetPos += finalAimTarget; + + // // targetDiffVec.Normalize(); + // // Vector3 lastTargetRelPos = (lastFinalAimTarget) - fireTransform.position; + + // safeToFire = BDATargetManager.CheckSafeToFireGuns(weaponManager, aimDirection, 1000, 0.999962f); //~0.5 degree of unsafe angle, was 0.999848f (1deg) + // if (safeToFire && targetCosAngle >= maxAutoFireCosAngle2) //check if directly on target + // { + // autoFire = true; + // } + // else + // { + // autoFire = false; + // } + // } + // else // rockets + // { + // safeToFire = BDATargetManager.CheckSafeToFireGuns(weaponManager, aimDirection, 1000, 0.999848f); + // if (safeToFire) + // { + // if ((Vector3.Distance(finalAimTarget, fireTransform.position) > blastRadius) && (targetCosAngle >= maxAutoFireCosAngle2)) + // { + // autoFire = true; //rockets already calculate where target will be + // } + // else + // { + // autoFire = false; + // } + // } + // } } else { @@ -3566,12 +4035,13 @@ void CheckAIAutofire() { if (autoFire && autofireShotCount >= fireBurstLength) { + if (Time.time - autoFireTimer > autoFireLength) autofireShotCount = 0; autoFire = false; - visualTargetVessel = null; - visualTargetPart = null; + //visualTargetVessel = null; //if there's no target, these get nulled in MissileFire. Nulling them here would cause Ai to stop engaging target with longer TargetScanIntervals as + //visualTargetPart = null; //there's no longer a targetVessel/part to do leadOffset aim calcs for. tgtShell = null; tgtRocket = null; - autofireShotCount = 0; + if (SpoolUpTime > 0) { roundsPerMinute = baseRPM / 10; @@ -3579,7 +4049,7 @@ void CheckAIAutofire() } if (eWeaponType == WeaponTypes.Laser && LaserGrowTime > 0) { - projectileColorC = Utils.ParseColor255(projectileColor); + projectileColorC = GUIUtils.ParseColor255(projectileColor); startColorS = startColor.Split(","[0]); laserDamage = baseLaserdamage; tracerStartWidth = tracerBaseSWidth; @@ -3590,13 +4060,13 @@ void CheckAIAutofire() } else { - if (autoFire && Time.time - autoFireTimer > autoFireLength) + if (autoFire && Time.time - autoFireTimer > autoFireLength && !isAPS) { autoFire = false; - visualTargetVessel = null; - visualTargetPart = null; - tgtShell = null; - tgtRocket = null; + //visualTargetVessel = null; + //visualTargetPart = null; + //tgtShell = null; + //tgtRocket = null; if (SpoolUpTime > 0) { roundsPerMinute = baseRPM / 10; @@ -3604,7 +4074,7 @@ void CheckAIAutofire() } if (eWeaponType == WeaponTypes.Laser && LaserGrowTime > 0) { - projectileColorC = Utils.ParseColor255(projectileColor); + projectileColorC = GUIUtils.ParseColor255(projectileColor); startColorS = startColor.Split(","[0]); laserDamage = baseLaserdamage; tracerStartWidth = tracerBaseSWidth; @@ -3613,21 +4083,90 @@ void CheckAIAutofire() } } } + if (isAPS) + { + float threatDirectionFactor = Vector3.Dot(fireTransforms[0].position, targetPosition.normalized); + if (threatDirectionFactor < 0.9f) autoFire = false; ; //within 28 degrees in front, else ignore, target likely not on intercept vector + } + } + + /// + /// Check for friendlies being likely to be hit by firing. + /// + /// true if no friendlies are likely to be hit, false otherwise. + bool CheckForFriendlies(Transform fireTransform) + { + if (weaponManager == null || weaponManager.vessel == null) return false; + var firingDirection = fireTransform.forward; + + if (eWeaponType == WeaponTypes.Laser) + { + using (var friendly = FlightGlobals.Vessels.GetEnumerator()) + while (friendly.MoveNext()) + { + if (VesselModuleRegistry.ignoredVesselTypes.Contains(friendly.Current.vesselType)) continue; + if (friendly.Current == null || friendly.Current == weaponManager.vessel) continue; + var wms = VesselModuleRegistry.GetModule(friendly.Current); + if (wms == null || wms.Team != weaponManager.Team) continue; + var friendlyRelativePosition = friendly.Current.CoM - fireTransform.position; + var theta = friendly.Current.GetRadius() / friendlyRelativePosition.magnitude; // Approx to arctan(θ) = θ - θ^3/3 + O(θ^5) + var cosTheta = Mathf.Clamp(1f - 0.5f * theta * theta, -1f, 1f); // Approximation to cos(theta) for the friendly vessel's radius at that distance. (cos(x) = 1-x^2/2!+O(x^4)) + if (Vector3.Dot(firingDirection, friendlyRelativePosition.normalized) > cosTheta) return false; // A friendly is in the way. + } + return true; + } + + // Projectile. Use bullet velocity or estimate of the rocket velocity post-thrust. + var projectileEffectiveVelocity = part.rb.velocity + (eWeaponType == WeaponTypes.Rocket ? (BDKrakensbane.FrameVelocityV3f + thrust * thrustTime / rocketMass * firingDirection) : (baseBulletVelocity * firingDirection)); + var gravity = (Vector3)FlightGlobals.getGeeForceAtPosition(fireTransform.position); // Use the local gravity value as long distance doesn't really matter here. + var projectileAcceleration = bulletDrop || eWeaponType == WeaponTypes.Rocket ? gravity : Vector3.zero; // Drag is ignored. + + using (var friendly = FlightGlobals.Vessels.GetEnumerator()) + while (friendly.MoveNext()) + { + if (VesselModuleRegistry.ignoredVesselTypes.Contains(friendly.Current.vesselType)) continue; + if (friendly.Current == null || friendly.Current == weaponManager.vessel) continue; + var wms = VesselModuleRegistry.GetModule(friendly.Current); + if (wms == null || wms.Team != weaponManager.Team) continue; + var friendlyPosition = friendly.Current.CoM; + var friendlyVelocity = friendly.Current.Velocity(); + var friendlyAcceleration = friendly.Current.acceleration; + var projectileRelativePosition = friendlyPosition - fireTransform.position; + var projectileRelativeVelocity = friendlyVelocity - projectileEffectiveVelocity; + var projectileRelativeAcceleration = friendlyAcceleration - projectileAcceleration; + var timeToCPA = AIUtils.TimeToCPA(projectileRelativePosition, projectileRelativeVelocity, projectileRelativeAcceleration, maxTargetingRange / projectileEffectiveVelocity.magnitude); + if (timeToCPA == 0) continue; // They're behind us. + var missDistanceSqr = AIUtils.PredictPosition(projectileRelativePosition, projectileRelativeVelocity, projectileRelativeAcceleration, timeToCPA).sqrMagnitude; + var tolerance = friendly.Current.GetRadius() + projectileRelativePosition.magnitude * Mathf.Deg2Rad * maxDeviation; // Use a firing tolerance of 1 and twice the projectile deviation for friendlies. + if (missDistanceSqr < tolerance * tolerance) return false; // A friendly is in the way. + } + return true; } void CheckFinalFire() { finalFire = false; //if user pulling the trigger || AI controlled and on target if turreted || finish a burstfire weapon's burst - if (((userFiring || agHoldFiring) && !isAPS) || (autoFire && (!turret || turret.TargetInRange(finalAimTarget, 10, float.MaxValue))) || (BurstFire && RoundsRemaining > 0 && RoundsRemaining < RoundsPerMag)) + if (fireConditionCheck) { - if ((pointingAtSelf || isOverheated || isReloading) || (aiControlled && engageRangeMax < targetDistance))// is weapon within set max range? + if ((pointingAtSelf || isOverheated || isReloading) || (aiControlled && (engageRangeMax < targetDistance || engageRangeMin > targetDistance)))// is weapon within set max range? { if (useRippleFire) //old method wouldn't catch non-ripple guns (i.e. Vulcan) trying to fire at targets beyond fire range { //StartCoroutine(IncrementRippleIndex(0)); - StartCoroutine(IncrementRippleIndex(initialFireDelay * TimeWarp.CurrentRate)); - isRippleFiring = true; + StartCoroutine(IncrementRippleIndex(InitialFireDelay * TimeWarp.CurrentRate)); //FIXME - possibly not getting called in all circumstances? Investigate later, future SI + //Debug.Log($"[BDarmory.moduleWeapon] Weapon on rippleindex {weaponManager.GetRippleIndex(WeaponName)} cant't fire, skipping to next weapon after a {initialFireDelay * TimeWarp.CurrentRate} sec delay"); + isRippleFiring = true; + } + if (eWeaponType == WeaponTypes.Laser) + { + if ((!pulseLaser && !BurstFire) || (!pulseLaser && BurstFire && (RoundsRemaining >= RoundsPerMag)) || (pulseLaser && Time.time - timeFired > beamDuration)) + { + for (int i = 0; i < laserRenderers.Length; i++) + { + laserRenderers[i].enabled = false; + } + } } } else @@ -3641,7 +4180,7 @@ void CheckFinalFire() roundsPerMinute = Mathf.Lerp((baseRPM / 10), baseRPM, spooltime); } } - if (!useRippleFire || weaponManager.gunRippleIndex == rippleIndex) // Don't fire rippling weapons when they're on the wrong part of the cycle. Spool up and grow lasers though. + if (!useRippleFire || isRippleFiring || weaponManager.GetRippleIndex(WeaponName) == rippleIndex) // Don't fire rippling weapons when they're on the wrong part of the cycle (initially; afterwards, let their timers decide). Spool up and grow lasers though. { finalFire = true; } @@ -3674,16 +4213,16 @@ void CheckFinalFire() } else { - if (weaponManager != null && weaponManager.gunRippleIndex == rippleIndex) + if (weaponManager != null && weaponManager.GetRippleIndex(WeaponName) == rippleIndex) { StartCoroutine(IncrementRippleIndex(0)); - isRippleFiring = false; + isRippleFiring = false; } if (eWeaponType == WeaponTypes.Laser) { if (LaserGrowTime > 0) { - projectileColorC = Utils.ParseColor255(projectileColor); + projectileColorC = GUIUtils.ParseColor255(projectileColor); startColorS = startColor.Split(","[0]); laserDamage = baseLaserdamage; tracerStartWidth = tracerBaseSWidth; @@ -3711,6 +4250,7 @@ void CheckFinalFire() roundsPerMinute = Mathf.Lerp(baseRPM, (baseRPM / 10), spooltime); } } + if (ChargeTime > 0) hasCharged = false; } } @@ -3718,11 +4258,11 @@ void AimAndFire() { // This runs in the FashionablyLate timing phase of FixedUpdate before Krakensbane corrections have been applied. if (!(aimAndFireIfPossible || aimOnly)) return; - if (this == null || weaponManager == null || FlightGlobals.currentMainBody == null) return; + if (this == null || weaponManager == null || !gameObject.activeInHierarchy || FlightGlobals.currentMainBody == null) return; if (isAPS) { - TrackIncomingprojectile(); + TrackIncomingProjectile(); } else { @@ -3730,7 +4270,6 @@ void AimAndFire() } if (targetAcquired) { - // updateAcceleration(targetVelocity, targetPosition, lastTargetAcquisitionType != targetAcquisitionType || (targetAcquisitionType == TargetAcquisitionType.Visual && lastVisualTargetVessel != visualTargetVessel)); smoothTargetKinematics(targetPosition, targetVelocity, targetAcceleration, lastTargetAcquisitionType != targetAcquisitionType || (targetAcquisitionType == TargetAcquisitionType.Visual && lastVisualTargetVessel != visualTargetVessel)); } @@ -3741,53 +4280,73 @@ void AimAndFire() CheckWeaponSafety(); CheckAIAutofire(); CheckFinalFire(); - // if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("DEBUG " + vessel.vesselName + " targeting visualTargetVessel: " + visualTargetVessel + ", finalFire: " + finalFire + ", pointingAtSelf: " + pointingAtSelf + ", targetDistance: " + targetDistance); + // if (BDArmorySettings.DEBUG_LABELS) Debug.Log("DEBUG " + vessel.vesselName + " targeting visualTargetVessel: " + visualTargetVessel + ", finalFire: " + finalFire + ", pointingAtSelf: " + pointingAtSelf + ", targetDistance: " + targetDistance); if (finalFire) { - switch (eWeaponType) + if (ChargeTime > 0 && !hasCharged) { - case WeaponTypes.Laser: - if (FireLaser()) + if (!isCharging) + { + if (chargeRoutine != null) { - for (int i = 0; i < laserRenderers.Length; i++) + StopCoroutine(chargeRoutine); + chargeRoutine = null; + } + chargeRoutine = StartCoroutine(ChargeRoutine()); + } + else + { + aimAndFireIfPossible = false; + aimOnly = false; + } + } + else + { + switch (eWeaponType) + { + case WeaponTypes.Laser: + if (FireLaser()) + { + for (int i = 0; i < laserRenderers.Length; i++) + { + laserRenderers[i].enabled = true; + } + if (isAPS && (tgtShell != null || tgtRocket != null)) + { + StartCoroutine(KillIncomingProjectile(tgtShell, tgtRocket)); + } + } + else { - laserRenderers[i].enabled = true; + if ((!pulseLaser && !BurstFire) || (!pulseLaser && BurstFire && (RoundsRemaining >= RoundsPerMag)) || (pulseLaser && Time.time - timeFired > beamDuration)) + { + for (int i = 0; i < laserRenderers.Length; i++) + { + laserRenderers[i].enabled = false; + } + } + //if (!pulseLaser || !oneShotSound) + //{ + // audioSource.Stop(); + //} } + break; + case WeaponTypes.Ballistic: + Fire(); if (isAPS && (tgtShell != null || tgtRocket != null)) { StartCoroutine(KillIncomingProjectile(tgtShell, tgtRocket)); } - } - else - { - if ((!pulseLaser && !BurstFire) || (!pulseLaser && BurstFire && (RoundsRemaining >= RoundsPerMag)) || (pulseLaser && Time.time - timeFired > beamDuration)) + break; + case WeaponTypes.Rocket: + FireRocket(); + if (isAPS && (tgtShell != null || tgtRocket != null)) { - for (int i = 0; i < laserRenderers.Length; i++) - { - laserRenderers[i].enabled = false; - } + StartCoroutine(KillIncomingProjectile(tgtShell, tgtRocket)); } - //if (!pulseLaser || !oneShotSound) - //{ - // audioSource.Stop(); - //} - } - break; - case WeaponTypes.Ballistic: - Fire(); - if (isAPS && (tgtShell != null || tgtRocket != null)) - { - StartCoroutine(KillIncomingProjectile(tgtShell, tgtRocket)); - } - break; - case WeaponTypes.Rocket: - FireRocket(); - if (isAPS && (tgtShell != null || tgtRocket != null)) - { - StartCoroutine(KillIncomingProjectile(tgtShell, tgtRocket)); - } - break; + break; + } } } } @@ -3807,28 +4366,28 @@ void DrawAlignmentIndicator() if (!refTransform) return; Vector3 fwdPos = fireTransforms[0].position + (5 * fireTransforms[0].forward); - BDGUIUtils.DrawLineBetweenWorldPositions(fireTransforms[0].position, fwdPos, 4, Color.green); + GUIUtils.DrawLineBetweenWorldPositions(fireTransforms[0].position, fwdPos, 4, Color.green); Vector3 referenceDirection = refTransform.up; Vector3 refUp = -refTransform.forward; Vector3 refRight = refTransform.right; Vector3 refFwdPos = fireTransforms[0].position + (5 * referenceDirection); - BDGUIUtils.DrawLineBetweenWorldPositions(fireTransforms[0].position, refFwdPos, 2, Color.white); + GUIUtils.DrawLineBetweenWorldPositions(fireTransforms[0].position, refFwdPos, 2, Color.white); - BDGUIUtils.DrawLineBetweenWorldPositions(fwdPos, refFwdPos, 2, XKCDColors.Orange); + GUIUtils.DrawLineBetweenWorldPositions(fwdPos, refFwdPos, 2, XKCDColors.Orange); Vector2 guiPos; - if (BDGUIUtils.WorldToGUIPos(fwdPos, out guiPos)) + if (GUIUtils.WorldToGUIPos(fwdPos, out guiPos)) { Rect angleRect = new Rect(guiPos.x, guiPos.y, 100, 200); - Vector3 pitchVector = (5 * Vector3.ProjectOnPlane(fireTransforms[0].forward, refRight)); - Vector3 yawVector = (5 * Vector3.ProjectOnPlane(fireTransforms[0].forward, refUp)); + Vector3 pitchVector = (5 * fireTransforms[0].forward.ProjectOnPlanePreNormalized(refRight)); + Vector3 yawVector = (5 * fireTransforms[0].forward.ProjectOnPlanePreNormalized(refUp)); - BDGUIUtils.DrawLineBetweenWorldPositions(fireTransforms[0].position + pitchVector, fwdPos, 3, + GUIUtils.DrawLineBetweenWorldPositions(fireTransforms[0].position + pitchVector, fwdPos, 3, Color.white); - BDGUIUtils.DrawLineBetweenWorldPositions(fireTransforms[0].position + yawVector, fwdPos, 3, Color.white); + GUIUtils.DrawLineBetweenWorldPositions(fireTransforms[0].position + yawVector, fwdPos, 3, Color.white); float pitch = Vector3.Angle(pitchVector, referenceDirection); float yaw = Vector3.Angle(yawVector, referenceDirection); @@ -3841,16 +4400,15 @@ void DrawAlignmentIndicator() float convergeAngle = 90 - Vector3.Angle(yawVector, refTransform.up); if (Vector3.Dot(fireTransforms[0].forward, projAxis) > 0) { - convergeDistance = "Converge: " + - Mathf.Round((xDist * Mathf.Tan(convergeAngle * Mathf.Deg2Rad))).ToString() + "m"; + convergeDistance = $"Converge: {Mathf.Round((xDist * Mathf.Tan(convergeAngle * Mathf.Deg2Rad))).ToString()} m"; } else { convergeDistance = "Diverging"; } - string xAngle = "X: " + Vector3.Angle(fireTransforms[0].forward, pitchVector).ToString("0.00"); - string yAngle = "Y: " + Vector3.Angle(fireTransforms[0].forward, yawVector).ToString("0.00"); + string xAngle = $"X: {Vector3.Angle(fireTransforms[0].forward, pitchVector):0.00}"; + string yAngle = $"Y: {Vector3.Angle(fireTransforms[0].forward, yawVector):0.00}"; GUI.Label(angleRect, xAngle + "\n" + yAngle + "\n" + convergeDistance); } @@ -3885,6 +4443,7 @@ void UpdateHeat() { isOverheated = true; autoFire = false; + hasCharged = false; if (!oneShotSound) audioSource.Stop(); wasFiring = false; audioSource2.PlayOneShot(overheatSound); @@ -3902,16 +4461,17 @@ void ReloadWeapon() { if (isReloading) { - ReloadTimer = Mathf.Clamp((ReloadTimer + 1 * TimeWarp.fixedDeltaTime / ReloadTime), 0, 1); + ReloadTimer = Mathf.Min(ReloadTimer + TimeWarp.fixedDeltaTime / (hasReloadAnim ? ReloadTime + fireAnimSpeed : ReloadTime), 1); if (hasDeployAnim) { - AnimTimer = Mathf.Clamp((AnimTimer + 1 * TimeWarp.fixedDeltaTime / (ReloadAnimTime)), 0, 1); + AnimTimer = Mathf.Min(AnimTimer + TimeWarp.fixedDeltaTime / (hasReloadAnim ? ReloadTime + fireAnimSpeed : ReloadTime), 1); } } if ((RoundsRemaining >= RoundsPerMag && !isReloading) && (ammoCount > 0 || BDArmorySettings.INFINITE_AMMO)) { isReloading = true; autoFire = false; + hasCharged = false; if (eWeaponType == WeaponTypes.Laser) { for (int i = 0; i < laserRenderers.Length; i++) @@ -3919,7 +4479,6 @@ void ReloadWeapon() laserRenderers[i].enabled = false; } } - if (!oneShotSound) audioSource.Stop(); wasFiring = false; weaponManager.ResetGuardInterval(); showReloadMeter = true; @@ -3934,8 +4493,16 @@ void ReloadWeapon() } else { - StopShutdownStartupRoutines(); - shutdownRoutine = StartCoroutine(ShutdownRoutine(true)); + if (hasDeployAnim) + { + StopShutdownStartupRoutines(); + shutdownRoutine = StartCoroutine(ShutdownRoutine(true)); + } + if (!oneShotSound) audioSource.Stop(); + if (!String.IsNullOrEmpty(reloadAudioPath)) + { + audioSource.PlayOneShot(reloadAudioClip); + } } } if (!hasReloadAnim && hasDeployAnim && (AnimTimer >= 1 && isReloading)) @@ -3967,12 +4534,18 @@ void ReloadWeapon() { UpdateRocketScales(); } + if (!String.IsNullOrEmpty(reloadCompletePath)) + { + audioSource.PlayOneShot(reloadCompleteAudioClip); + } } } void UpdateTargetVessel() { targetAcquired = false; slaved = false; + GPSTarget = false; + radarTarget = false; bool atprWasAcquired = atprAcquired; atprAcquired = false; lastTargetAcquisitionType = targetAcquisitionType; @@ -3999,21 +4572,23 @@ void UpdateTargetVessel() if (aiControlled && weaponManager && visualTargetVessel && (visualTargetVessel.transform.position - transform.position).sqrMagnitude < weaponManager.guardRange * weaponManager.guardRange) { - targetRadius = visualTargetVessel.GetRadius(); + //targetRadius = visualTargetVessel.GetRadius(); if (visualTargetPart == null || visualTargetPart.vessel != visualTargetVessel) { TargetInfo currentTarget = visualTargetVessel.gameObject.GetComponent(); if (currentTarget == null) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.ModuleWeapon]: Targeted vessel " + (visualTargetVessel != null ? visualTargetVessel.vesselName : "'unknown'") + " has no TargetInfo."); + if (BDArmorySettings.DEBUG_WEAPONS) Debug.Log($"[BDArmory.ModuleWeapon]: Targeted vessel {(visualTargetVessel != null ? visualTargetVessel.vesselName : "'unknown'")} has no TargetInfo."); return; } + //targetRadius = visualTargetVessel.GetRadius(fireTransforms[0].forward, currentTarget.bounds); List targetparts = new List(); if (targetCOM) { targetPosition = visualTargetVessel.CoM; visualTargetPart = null; //make sure this gets reset + targetRadius = visualTargetVessel.GetRadius(fireTransforms[0].forward, currentTarget.bounds); } else { @@ -4057,6 +4632,17 @@ void UpdateTargetVessel() } } } + if (targetRandom) + { + for (int i = 0; i < Mathf.Min(currentTarget.Vessel.Parts.Count, weaponManager.multiTargetNum); i++) + { + int r = (int)UnityEngine.Random.Range(0, Mathf.Min(currentTarget.Vessel.Parts.Count, weaponManager.multiTargetNum)); + if (!targetparts.Contains(currentTarget.Vessel.Parts[r])) + { + targetparts.Add(currentTarget.Vessel.Parts[r]); + } + } + } if (!targetCOM && !targetCockpits && !targetEngines && !targetWeapons && !targetMass) { for (int i = 0; i < currentTarget.targetMassList.Count; i++) @@ -4080,13 +4666,15 @@ void UpdateTargetVessel() } if (targetparts.Count == 0) { - if (BDArmorySettings.DRAW_DEBUG_LABELS) Debug.Log("[BDArmory.ModuleWeapon]: Targeted vessel " + visualTargetVessel.vesselName + " has no targetable parts."); + if (BDArmorySettings.DEBUG_WEAPONS) Debug.Log($"[BDArmory.ModuleWeapon]: Targeted vessel {visualTargetVessel.vesselName} has no targetable parts."); targetPosition = visualTargetVessel.CoM; + targetRadius = visualTargetVessel.GetRadius(fireTransforms[0].forward, currentTarget.bounds); } else { visualTargetPart = targetparts[targetID]; targetPosition = visualTargetPart.transform.position; + targetRadius = 3; //allow for more focused targeting of weighted subsystems } } } @@ -4096,10 +4684,12 @@ void UpdateTargetVessel() { targetPosition = visualTargetVessel.CoM; visualTargetPart = null; //make sure these get reset + targetRadius = visualTargetVessel.GetRadius(); } else { targetPosition = visualTargetPart.transform.position; + targetRadius = 5; } } targetVelocity = visualTargetVessel.rb_velocity; @@ -4114,7 +4704,7 @@ void UpdateTargetVessel() slaved = true; targetRadius = weaponManager.slavedTarget.vessel != null ? weaponManager.slavedTarget.vessel.GetRadius() : 35f; targetPosition = weaponManager.slavedPosition; - targetVelocity = weaponManager.slavedTarget.vessel != null ? weaponManager.slavedTarget.vessel.rb_velocity : (weaponManager.slavedVelocity - Krakensbane.GetFrameVelocityV3f()); + targetVelocity = weaponManager.slavedTarget.vessel != null ? weaponManager.slavedTarget.vessel.rb_velocity : (weaponManager.slavedVelocity - BDKrakensbane.FrameVelocityV3f); //targetAcceleration = weaponManager.slavedTarget.vessel != null ? weaponManager.slavedTarget.vessel.acceleration : weaponManager.slavedAcceleration; //CS0172 Type of conditional expression cannot be determined because 'Vector3' and 'Vector3' implicitly convert to one another if (weaponManager.slavedTarget.vessel != null) targetAcceleration = weaponManager.slavedTarget.vessel.acceleration; @@ -4128,7 +4718,7 @@ void UpdateTargetVessel() if (weaponManager.vesselRadarData && weaponManager.vesselRadarData.locked) { TargetSignatureData targetData = weaponManager.vesselRadarData.lockedTargetData.targetData; - targetVelocity = targetData.velocity - Krakensbane.GetFrameVelocityV3f(); + targetVelocity = targetData.velocity - BDKrakensbane.FrameVelocityV3f; targetPosition = targetData.predictedPosition; targetRadius = 35f; targetAcceleration = targetData.acceleration; @@ -4141,6 +4731,20 @@ void UpdateTargetVessel() } targetAcquired = true; targetAcquisitionType = TargetAcquisitionType.Radar; + radarTarget = true; + return; + } + + // GPS TARGETING HERE + if (BDArmorySetup.Instance.showingWindowGPS && weaponManager.designatedGPSCoords != Vector3d.zero && !aiControlled) + { + GPSTarget = true; + targetVelocity = Vector3d.zero; + targetPosition = weaponManager.designatedGPSInfo.worldPos; + targetRadius = 35f; + targetAcceleration = Vector3d.zero; + targetAcquired = true; + targetAcquisitionType = TargetAcquisitionType.GPS; return; } @@ -4186,22 +4790,28 @@ void UpdateTargetVessel() targetAcceleration = tgt.acceleration; } targetAcquisitionType = TargetAcquisitionType.AutoProxy; + return; } } + + if (!targetAcquired) + { + targetVelocity = Vector3.zero; + targetAcceleration = Vector3.zero; + } } - void TrackIncomingprojectile() //this is holding onto initial target for some reason, not properly nulling target somewher it should be nulled + void TrackIncomingProjectile() { targetAcquired = false; slaved = false; atprAcquired = false; lastTargetAcquisitionType = targetAcquisitionType; - + closestTarget = Vector3.zero; if (Time.time - lastGoodTargetTime > Mathf.Max(roundsPerMinute / 60f, weaponManager.targetScanInterval)) { targetAcquisitionType = TargetAcquisitionType.None; } - if (weaponManager && aiControlled) { //if (tgtShell == null && tgtRocket == null && MissileTgt == null) @@ -4214,7 +4824,9 @@ void UpdateTargetVessel() while (target.MoveNext()) { if (target.Current == null) continue; - + if (target.Current.team == weaponManager.team) continue; + float threatDirectionFactor = Vector3.Dot((transform.position - target.Current.transform.position).normalized, target.Current.currentVelocity.normalized); + if (threatDirectionFactor < 0.95) continue; //if incoming round is heading this way if ((target.Current.currPosition - fireTransforms[0].position).sqrMagnitude < (maxTargetingRange * 2) * (maxTargetingRange * 2)) { if (RadarUtils.TerrainCheck(target.Current.transform.position, transform.position)) @@ -4223,18 +4835,15 @@ void UpdateTargetVessel() } else { - if (target.Current.team != weaponManager.team) + if ((closestTarget == Vector3.zero || (target.Current.transform.position - fireTransforms[0].position).sqrMagnitude < (closestTarget - fireTransforms[0].position).sqrMagnitude)) { - if ((closestTarget == Vector3.zero || (target.Current.transform.position - fireTransforms[0].position).sqrMagnitude < (closestTarget - fireTransforms[0].position).sqrMagnitude)) - { - closestTarget = target.Current.currPosition; - //tgtVelocity = target.Current.currentVelocity; - tgtShell = target.Current; - visualTargetPart = null; //make sure this gets reset - tgtRocket = null; - } + + closestTarget = target.Current.currPosition; + //tgtVelocity = target.Current.currentVelocity; + tgtShell = target.Current; + visualTargetPart = null; //make sure this gets reset + tgtRocket = null; } - else continue; } } } @@ -4249,7 +4858,9 @@ void UpdateTargetVessel() while (target.MoveNext()) { if (target.Current == null) continue; - + if (target.Current.team == weaponManager.team) continue; + float threatDirectionFactor = Vector3.Dot((transform.position - target.Current.transform.position).normalized, target.Current.currentVelocity.normalized); + if (threatDirectionFactor < 0.95) continue; //if incoming round is heading this way if ((target.Current.transform.position - fireTransforms[0].position).sqrMagnitude < (maxTargetingRange * 2) * (maxTargetingRange * 2)) { if (RadarUtils.TerrainCheck(target.Current.transform.position, transform.position)) @@ -4258,18 +4869,14 @@ void UpdateTargetVessel() } else { - if (target.Current.team != weaponManager.team) + if ((closestTarget == Vector3.zero || (target.Current.transform.position - fireTransforms[0].position).sqrMagnitude < (closestTarget - fireTransforms[0].position).sqrMagnitude)) { - if ((closestTarget == Vector3.zero || (target.Current.transform.position - fireTransforms[0].position).sqrMagnitude < (closestTarget - fireTransforms[0].position).sqrMagnitude)) - { - closestTarget = target.Current.transform.position; - //tgtVelocity = target.Current.currentVelocity; - tgtRocket = target.Current; - tgtShell = null; - visualTargetPart = null; //make sure this gets reset - } + closestTarget = target.Current.transform.position; + //tgtVelocity = target.Current.currentVelocity; + tgtRocket = target.Current; + tgtShell = null; + visualTargetPart = null; //make sure this gets reset } - else continue; } } } @@ -4278,50 +4885,62 @@ void UpdateTargetVessel() if (BDATargetManager.FiredMissiles.Count > 0) { MissileTgt = BDATargetManager.GetClosestMissileTarget(weaponManager); - if ((MissileTgt.transform.position - fireTransforms[0].position).sqrMagnitude < (maxTargetingRange * 2) * (maxTargetingRange * 2)) + if (MissileTgt != null) { - if ((closestTarget == Vector3.zero || (MissileTgt.transform.position - fireTransforms[0].position).sqrMagnitude < (closestTarget - fireTransforms[0].position).sqrMagnitude)) + if ((MissileTgt.transform.position - fireTransforms[0].position).sqrMagnitude < (maxTargetingRange * 2) * (maxTargetingRange * 2)) { - closestTarget = MissileTgt.transform.position; - //tgtVelocity = MissileTgt.velocity; - visualTargetPart = MissileTgt.Vessel.Parts.FirstOrDefault(); - tgtShell = null; - tgtRocket = null; + if ((closestTarget == Vector3.zero || (MissileTgt.transform.position - fireTransforms[0].position).sqrMagnitude < (closestTarget - fireTransforms[0].position).sqrMagnitude)) + { + closestTarget = MissileTgt.transform.position; + //tgtVelocity = MissileTgt.velocity; + visualTargetPart = MissileTgt.Vessel.Parts.FirstOrDefault(); + tgtShell = null; + tgtRocket = null; + } } } } else visualTargetPart = null; } } - if (tgtShell != null || tgtRocket != null || MissileTgt != null) + if (tgtShell != null || tgtRocket != null || visualTargetPart != null) { + visualTargetVessel = null; if (tgtShell != null) { targetVelocity = tgtShell.currentVelocity; targetPosition = tgtShell.currPosition; + targetRadius = 0.25f; } - else if (tgtRocket != null) + if (tgtRocket != null) { targetVelocity = tgtRocket.currentVelocity; targetPosition = tgtRocket.currPosition; + targetRadius = 0.25f; } - else + if (visualTargetPart != null) { targetVelocity = visualTargetPart.vessel.rb_velocity; targetPosition = visualTargetPart.transform.position; + visualTargetVessel = visualTargetPart.vessel; + TargetInfo currentTarget = visualTargetVessel.gameObject.GetComponent(); + targetRadius = visualTargetVessel.GetRadius(fireTransforms[0].forward, currentTarget.bounds); } - targetVelocity -= Krakensbane.GetFrameVelocityV3f(); - targetRadius = 1; + //targetVelocity -= BDKrakensbane.FrameVelocity; - targetAcceleration = MissileTgt != null && MissileTgt.Vessel != null ? (Vector3)MissileTgt.Vessel.acceleration : Vector3.zero; + targetAcceleration = visualTargetPart != null && visualTargetPart.vessel != null ? (Vector3)visualTargetPart.vessel.acceleration : Vector3.zero; targetAcquired = true; targetAcquisitionType = TargetAcquisitionType.Radar; - - Debug.Log("[APS DEBUG] tgtVelocity: " + tgtVelocity + "; tgtPosition: " + targetPosition + "; tgtAccel: " + targetAcceleration); - Debug.Log("[APS DEBUG] Lead Offset: " + fixedLeadOffset + ", FinalAimTgt: " + finalAimTarget + ", tgt CosAngle " + targetCosAngle + ", wpn CosAngle " + targetAdjustedMaxCosAngle + ", Wpn Autofire: " + autoFire); + if (weaponManager.slavingTurrets && turret) slaved = true; + //Debug.Log("[APS DEBUG] tgtVelocity: " + tgtVelocity + "; tgtPosition: " + targetPosition + "; tgtAccel: " + targetAcceleration); + //Debug.Log("[APS DEBUG] Lead Offset: " + fixedLeadOffset + ", FinalAimTgt: " + finalAimTarget + ", tgt CosAngle " + targetCosAngle + ", wpn CosAngle " + targetAdjustedMaxCosAngle + ", Wpn Autofire: " + autoFire); return; } - //Need to get bullet trajectory - is it on a collision course? Moght be able to plug into the TimeToCPA to see if it's going to hit, and if not, ignore it + else + { + if (turret) turret.ReturnTurret(); //reset turret if no target + visualTargetVessel = null; + } } } @@ -4331,104 +4950,78 @@ IEnumerator KillIncomingProjectile(PooledBullet shell, PooledRocket rocket) //should include a check for non-explosive rounds merely getting knocked off course instead of exploded. //should this be shell size dependant? I.e. sure, an APS can knock a sabot offcourse with a 60mm interceptor; what about that same 60mm shot vs a 155mm arty shell? or a 208mm naval gun? //really only an issue in case of AP APS (e.g. flechette APS for anti-missile work) vs AP shell; HE APS rounds should be able to destroy inncoming proj - float delayTime = eWeaponType == WeaponTypes.Ballistic ? (targetDistance / bulletVelocity) : (eWeaponType == WeaponTypes.Rocket ? predictedFlightTime : -1); - if (delayTime < 0) + if (shell != null || rocket != null) { - delayTime = rocket != null ? 0.5f : (shell.bulletMass * (1 - Mathf.Clamp(shell.tntMass / shell.bulletMass, 0f, 0.95f) / 2)); //for shells, laser delay time is based on shell mass/HEratio. The heavier the shell, the mroe mass to burn through. Don't expect to stop sabots via laser APS - delayTime /= ((laserDamage / (1 + Mathf.PI * Mathf.Pow(tanAngle * targetDistance, 2)) * 0.425f) / 100); - if (delayTime < TimeWarp.fixedDeltaTime) delayTime = 0; - } - yield return new WaitForSeconds(delayTime); - if (shell != null) - { - if (shell.tntMass > 0) + delayTime = -1; + if (baseDeviation > 0.05 && (eWeaponType == WeaponTypes.Ballistic || (eWeaponType == WeaponTypes.Laser && pulseLaser))) //if using rotary cannon/CIWS for APS { - shell.hasDetonated = true; - ExplosionFx.CreateExplosion(shell.transform.position, shell.tntMass, shell.explModelPath, shell.explSoundPath, ExplosionSourceType.Bullet, shell.caliber, null, shell.sourceVesselName, null, default, -1, false, shell.bulletMass, -1, 1); - shell.KillBullet(); - Debug.Log("[BDArmory.ModuleWeapon] Detonated Incoming Projectile!"); + if (UnityEngine.Random.Range(0, (targetDistance - (Mathf.Cos(baseDeviation) * targetDistance))) > 1) + { + yield break; //simulate inaccuracy, decreasing as incoming projectile gets closer + } } - else + delayTime = eWeaponType == WeaponTypes.Ballistic ? (targetDistance / (bulletVelocity + targetVelocity.magnitude)) : (eWeaponType == WeaponTypes.Rocket ? (targetDistance / ((targetDistance / predictedFlightTime) + targetVelocity.magnitude)) : -1); + if (delayTime < 0) { - if (eWeaponType == WeaponTypes.Laser) + delayTime = rocket != null ? 0.5f : (shell.bulletMass * (1 - Mathf.Clamp(shell.tntMass / shell.bulletMass, 0f, 0.95f) / 2)); //for shells, laser delay time is based on shell mass/HEratio. The heavier the shell, the mroe mass to burn through. Don't expect to stop sabots via laser APS + var angularSpread = tanAngle * targetDistance; + delayTime /= ((laserDamage / (1 + Mathf.PI * angularSpread * angularSpread) * 0.425f) / 100); + if (delayTime < TimeWarp.fixedDeltaTime) delayTime = 0; + } + yield return new WaitForSeconds(delayTime); + if (shell != null) + { + if (shell.tntMass > 0) { + shell.hasDetonated = true; + ExplosionFx.CreateExplosion(shell.transform.position, shell.tntMass, shell.explModelPath, shell.explSoundPath, ExplosionSourceType.Bullet, shell.caliber, null, shell.sourceVesselName, null, default, -1, false, shell.bulletMass, -1, 1); shell.KillBullet(); - Debug.Log("[BDArmory.ModuleWeapon] Vaporized Incoming Projectile!"); + tgtShell = null; + if (BDArmorySettings.DEBUG_WEAPONS) Debug.Log("[BDArmory.ModuleWeapon] Detonated Incoming Projectile!"); } else { - if (tntMass <= 0) //e.g. APS flechettes vs sabot + if (eWeaponType == WeaponTypes.Laser) { - shell.bulletMass -= bulletMass; - shell.currentVelocity = VectorUtils.GaussianDirectionDeviation(shell.currentVelocity, ((shell.bulletMass * shell.currentVelocity.magnitude) / (bulletMass * bulletVelocity))); - //shell.caliber = //have some modification of caliber to sim knocking round off-prograde? - //Thing is, something like a sabot liable to have lever action work upon it, spin it so it now hits on it's side instead of point first, but a heavy arty shell you have both substantially greater mass to diflect, and lesser increase in caliber from perpendicular hit - sabot from point on to side on is like a ~10x increase, a 208mm shell is like 1.2x - //there's also the issue of gross modification of caliber in this manner if the shell recieves multiple impacts from APS interceptors before it hits; would either need to be caliber = x, which isn't appropraite for heavy shells that would not be easily knocked off course, or caliber +=, which isn't viable for sabots - //easiest way would just have the APS interceptor destroy the incoming round, regardless; and just accept the occasional edge cases like a flechetteammo APS being able to destroy AP naval shells instead of tickling them and not much else + shell.KillBullet(); + tgtShell = null; + if (BDArmorySettings.DEBUG_WEAPONS) Debug.Log("[BDArmory.ModuleWeapon] Vaporized Incoming Projectile!"); } else { - shell.KillBullet(); - Debug.Log("[BDArmory.ModuleWeapon] Exploded Incoming Projectile!"); + if (tntMass <= 0) //e.g. APS flechettes vs sabot + { + shell.bulletMass -= bulletMass; + shell.currentVelocity = VectorUtils.GaussianDirectionDeviation(shell.currentVelocity, ((shell.bulletMass * shell.currentVelocity.magnitude) / (bulletMass * bulletVelocity))); + //shell.caliber = //have some modification of caliber to sim knocking round off-prograde? + //Thing is, something like a sabot liable to have lever action work upon it, spin it so it now hits on it's side instead of point first, but a heavy arty shell you have both substantially greater mass to diflect, and lesser increase in caliber from perpendicular hit - sabot from point on to side on is like a ~10x increase, a 208mm shell is like 1.2x + //there's also the issue of gross modification of caliber in this manner if the shell receives multiple impacts from APS interceptors before it hits; would either need to be caliber = x, which isn't appropraite for heavy shells that would not be easily knocked off course, or caliber +=, which isn't viable for sabots + //easiest way would just have the APS interceptor destroy the incoming round, regardless; and just accept the occasional edge cases like a flechetteammo APS being able to destroy AP naval shells instead of tickling them and not much else + } + else + { + shell.KillBullet(); + tgtShell = null; + if (BDArmorySettings.DEBUG_WEAPONS) Debug.Log("[BDArmory.ModuleWeapon] Exploded Incoming Projectile!"); + } } } } - shell = null; - tgtShell = null; - } - else if (rocket != null) - { - if (rocket.tntMass > 0) - { - rocket.hasDetonated = true; - ExplosionFx.CreateExplosion(rocket.transform.position, rocket.tntMass, rocket.explModelPath, rocket.explSoundPath, ExplosionSourceType.Rocket, rocket.caliber, null, rocket.sourceVesselName, null, default, -1, false, rocket.rocketMass * 1000, -1, 1); - } - rocket.gameObject.SetActive(false); - rocket = null; - tgtRocket = null; - } - else - { - Debug.Log("[BDArmory.ModuleWeapon] KillIncomingProjectile called on null object!"); - } - } - - /// - /// Update target acceleration based on previous velocity. - /// Position is used to clamp acceleration for splashed targets, as ksp produces excessive bobbing. - /// - void updateAcceleration(Vector3 target_rb_velocity, Vector3 position, bool reset = false) - { - var distance = Vector3.Distance(targetPosition, part.transform.position); - var accelerationSmoothingFactor = 512f / (Mathf.Max(distance, 512f)); // Dynamically adjust the smoothing from none at 512m to 127/128 at 65.536km - var velocitySmoothingFactor = 2048f / (Mathf.Max(distance, 2048f)); // Dynamically adjust the smoothing from none at 2048m to 31/32 at 65.536km - if (!reset) - { - if (!FloatingOrigin.Offset.IsZero() || !Krakensbane.GetFrameVelocity().IsZero()) - { - targetVelocityPrevious += (Vector3)Krakensbane.GetLastCorrection(); - } - if (!targetAccelerationWasReset) - { - targetVelocity = (1f - velocitySmoothingFactor) * targetVelocityPrevious + velocitySmoothingFactor * target_rb_velocity; // Smooth out the velocity to account for imprecise velocity from KSP. - targetAcceleration = (1f - accelerationSmoothingFactor) * targetAcceleration + accelerationSmoothingFactor * (target_rb_velocity - targetVelocityPrevious) / (Time.time - lastGoodTargetTime); // Smooth out the acceleration calculation to account for oscillating control surfaces in the target. - } else { - targetAcceleration = (target_rb_velocity - targetVelocityPrevious) / (Time.time - lastGoodTargetTime); - targetAccelerationWasReset = false; + if (rocket.tntMass > 0) + { + rocket.hasDetonated = true; + ExplosionFx.CreateExplosion(rocket.transform.position, rocket.tntMass, rocket.explModelPath, rocket.explSoundPath, ExplosionSourceType.Rocket, rocket.caliber, null, rocket.sourceVesselName, null, default, -1, false, rocket.rocketMass * 1000, -1, 1); + } + rocket.gameObject.SetActive(false); + tgtRocket = null; } } else { - targetAcceleration = Vector3.zero; - targetAccelerationWasReset = true; + //Debug.Log("[BDArmory.ModuleWeapon] KillIncomingProjectile called on null object!"); } - float altitude = (float)FlightGlobals.currentMainBody.GetAltitude(position); - if (altitude < 12 && altitude > -10) - targetAcceleration = Vector3.ProjectOnPlane(targetAcceleration, VectorUtils.GetUpDirection(position)); - // Debug.Log("DEBUG " + Time.time.ToString("0.00") + " targetAcceleration: " + targetAcceleration.ToString("G3") + ", V_diff: " + (targetVelocity - targetVelocityPrevious).ToString("G3") + ", smoothing: (" + velocitySmoothingFactor.ToString("G3") + ", " + accelerationSmoothingFactor.ToString("G3") + "), Δt: " + (Time.time - lastGoodTargetTime)); - targetVelocityPrevious = target_rb_velocity; } /// @@ -4444,10 +5037,10 @@ void smoothTargetKinematics(Vector3 position, Vector3 velocity, Vector3 accelera // Floating objects need vertical smoothing. float altitude = (float)FlightGlobals.currentMainBody.GetAltitude(position); if (altitude < 12 && altitude > -10) - acceleration = Vector3.ProjectOnPlane(acceleration, VectorUtils.GetUpDirection(position)); + acceleration = acceleration.ProjectOnPlanePreNormalized(VectorUtils.GetUpDirection(position)); var distance = Vector3.Distance(position, part.transform.position); - var alpha = Mathf.Max(1f - Mathf.Sqrt(distance) / 512f, 0.1f); + var alpha = Mathf.Max(1f - BDAMath.Sqrt(distance) / 512f, 0.1f); var beta = alpha * alpha; if (!reset) { @@ -4474,46 +5067,46 @@ void UpdateGUIWeaponState() guiStatusString = weaponState.ToString(); } - IEnumerator StartupRoutine(bool calledByReload = false) + IEnumerator StartupRoutine(bool calledByReload = false, bool secondaryFiring = false) { if (hasReloadAnim && isReloading) //wait for reload to finish before shutting down { - while (reloadState.normalizedTime < 1) - { - yield return null; - } + yield return new WaitWhileFixed(() => reloadState.normalizedTime < 1); } if (!calledByReload) { + weaponState = WeaponStates.PoweringUp; UpdateGUIWeaponState(); } if (hasDeployAnim && deployState) { deployState.enabled = true; deployState.speed = 1; - while (deployState.normalizedTime < 1) //wait for animation here - { - yield return null; - } + yield return new WaitWhileFixed(() => deployState.normalizedTime < 1); //wait for animation here deployState.normalizedTime = 1; deployState.speed = 0; deployState.enabled = false; } if (!calledByReload) { - weaponState = WeaponStates.Enabled; + if (!secondaryFiring) + weaponState = WeaponStates.Enabled; + else + weaponState = WeaponStates.EnabledForSecondaryFiring; } UpdateGUIWeaponState(); BDArmorySetup.Instance.UpdateCursorState(); + if (isAPS && (ammoCount > 0 || !BDArmorySettings.INFINITE_AMMO)) + { + aiControlled = true; + targetPosition = fireTransforms[0].forward * engageRangeMax; //Ensure targetPosition is not null or 0 by the time code reaches Aim(), in case of no incoming projectile, since no target vessel to be continuously tracked. + } } IEnumerator ShutdownRoutine(bool calledByReload = false) { if (hasReloadAnim && isReloading) //wait for relaod to finish before shutting down { - while (reloadState.normalizedTime < 1) - { - yield return null; - } + yield return new WaitWhileFixed(() => reloadState.normalizedTime < 1); } if (!calledByReload) //allow isreloading to co-opt the startup/shutdown anim without disabling weapon in the process { @@ -4527,21 +5120,14 @@ IEnumerator ShutdownRoutine(bool calledByReload = false) BDArmorySetup.Instance.UpdateCursorState(); if (turret) { - yield return new WaitForSeconds(0.2f); - - while (!turret.ReturnTurret()) //wait till turret has returned - { - yield return new WaitForFixedUpdate(); - } + yield return new WaitForSecondsFixed(0.2f); + yield return new WaitWhileFixed(() => !turret.ReturnTurret()); //wait till turret has returned } if (hasDeployAnim) { deployState.enabled = true; deployState.speed = -1; - while (deployState.normalizedTime > 0) - { - yield return null; - } + yield return new WaitWhileFixed(() => deployState.normalizedTime > 0); deployState.normalizedTime = 0; deployState.speed = 0; deployState.enabled = false; @@ -4555,20 +5141,82 @@ IEnumerator ShutdownRoutine(bool calledByReload = false) IEnumerator ReloadRoutine() { guiStatusString = "Reloading"; - - reloadState.normalizedTime = 0; - reloadState.enabled = true; - reloadState.speed = (reloadState.length / ReloadTime);//ensure relaod anim is not longer than reload time - while (reloadState.normalizedTime < 1) //wait for animation here + yield return new WaitForSecondsFixed(fireAnimSpeed); //wait for fire anim to finish. + for (int i = 0; i < fireState.Length; i++) { - yield return null; + fireState[i].normalizedTime = 0; + fireState[i].speed = 0; + fireState[i].enabled = false; } + if (!oneShotSound) audioSource.Stop(); + if (!String.IsNullOrEmpty(reloadAudioPath)) + { + audioSource.PlayOneShot(reloadAudioClip); + } + reloadState.normalizedTime = 0; + reloadState.enabled = true; + reloadState.speed = (reloadState.length / ReloadTime);//ensure reload anim is not longer than reload time + yield return new WaitWhileFixed(() => reloadState.normalizedTime < 1); //wait for animation here reloadState.normalizedTime = 1; reloadState.speed = 0; reloadState.enabled = false; UpdateGUIWeaponState(); } + IEnumerator ChargeRoutine() + { + isCharging = true; + guiStatusString = "Charging"; + if (!String.IsNullOrEmpty(chargeSoundPath)) + { + audioSource.PlayOneShot(chargeSound); + } + if (hasChargeAnimation) + { + chargeState.normalizedTime = 0; + chargeState.enabled = true; + chargeState.speed = (chargeState.length / ChargeTime);//ensure relaod anim is not longer than reload time + yield return new WaitWhileFixed(() => chargeState.normalizedTime < 1); //wait for animation here + chargeState.normalizedTime = 1; + chargeState.speed = 0; + chargeState.enabled = false; + } + else + { + yield return new WaitForSecondsFixed(ChargeTime); + } + UpdateGUIWeaponState(); + isCharging = false; + if (!ChargeEachShot) hasCharged = true; + switch (eWeaponType) + { + case WeaponTypes.Laser: + if (FireLaser()) + { + for (int i = 0; i < laserRenderers.Length; i++) + { + laserRenderers[i].enabled = true; + } + } + else + { + if ((!pulseLaser && !BurstFire) || (!pulseLaser && BurstFire && (RoundsRemaining >= RoundsPerMag)) || (pulseLaser && Time.time - timeFired > beamDuration)) + { + for (int i = 0; i < laserRenderers.Length; i++) + { + laserRenderers[i].enabled = false; + } + } + } + break; + case WeaponTypes.Ballistic: + Fire(); + break; + case WeaponTypes.Rocket: + FireRocket(); + break; + } + } IEnumerator StandbyRoutine() { yield return StartupRoutine(true); @@ -4654,6 +5302,26 @@ void ParseBulletFuzeType(string type) break; } } + void ParseBulletHEType(string type) + { + type = type.ToLower(); + switch (type) + { + case "standard": + eHEType = FillerTypes.Standard; + break; + //legacy support for older configs that are still explosive = true + case "true": + eHEType = FillerTypes.Standard; + break; + case "shaped": + eHEType = FillerTypes.Shaped; + break; + default: + eHEType = FillerTypes.None; + break; + } + } void ParseAPSType(string type) { type = type.ToLower(); @@ -4733,13 +5401,16 @@ public void ParseAmmoStats() bulletMass = bulletInfo.bulletMass; ProjectileCount = bulletInfo.subProjectileCount; bulletDragTypeName = bulletInfo.bulletDragTypeName; - projectileColorC = Utils.ParseColor255(bulletInfo.projectileColor); - startColorC = Utils.ParseColor255(bulletInfo.startColor); + projectileColorC = GUIUtils.ParseColor255(bulletInfo.projectileColor); + startColorC = GUIUtils.ParseColor255(bulletInfo.startColor); fadeColor = bulletInfo.fadeColor; ParseBulletDragType(); ParseBulletFuzeType(bulletInfo.fuzeType); + ParseBulletHEType(bulletInfo.explosive); tntMass = bulletInfo.tntMass; beehive = bulletInfo.beehive; + Impulse = bulletInfo.impulse; + massAdjustment = bulletInfo.massMod; if (!tracerOverrideWidth) { tracerStartWidth = caliber / 300; @@ -4760,55 +5431,58 @@ public void ParseAmmoStats() baseBulletVelocity = bulletVelocity; if (bulletInfo.subProjectileCount > 1) { - guiAmmoTypeString = Localizer.Format("#LOC_BDArmory_Ammo_Shot") + " "; + guiAmmoTypeString = StringUtils.Localize("#LOC_BDArmory_Ammo_Shot") + " "; //maxDeviation *= Mathf.Clamp(bulletInfo.subProjectileCount/5, 2, 5); //modify deviation if shot vs slug AccAdjust(null, null); } - if (bulletInfo.apBulletMod >= 1.1) + if (bulletInfo.apBulletMod >= 1.1 || SabotRound) { - guiAmmoTypeString += Localizer.Format("#LOC_BDArmory_Ammo_AP") + " "; + guiAmmoTypeString += StringUtils.Localize("#LOC_BDArmory_Ammo_AP") + " "; } else if (bulletInfo.apBulletMod < 1.1 && bulletInfo.apBulletMod > 0.8f) { - guiAmmoTypeString += Localizer.Format("#LOC_BDArmory_Ammo_SAP") + " "; + guiAmmoTypeString += StringUtils.Localize("#LOC_BDArmory_Ammo_SAP") + " "; } if (bulletInfo.nuclear) { - guiAmmoTypeString += Localizer.Format("#LOC_BDArmory_Ammo_Nuclear") + " "; + guiAmmoTypeString += StringUtils.Localize("#LOC_BDArmory_Ammo_Nuclear") + " "; } - if (bulletInfo.explosive && !bulletInfo.nuclear) + if (bulletInfo.tntMass > 0 && !bulletInfo.nuclear) { if (eFuzeType == FuzeTypes.Timed || eFuzeType == FuzeTypes.Proximity || eFuzeType == FuzeTypes.Flak) { - guiAmmoTypeString += Localizer.Format("#LOC_BDArmory_Ammo_Flak") + " "; + guiAmmoTypeString += StringUtils.Localize("#LOC_BDArmory_Ammo_Flak") + " "; } - guiAmmoTypeString += Localizer.Format("#LOC_BDArmory_Ammo_Explosive") + " "; + else if (eHEType == FillerTypes.Shaped) + { + guiAmmoTypeString += StringUtils.Localize("#LOC_BDArmory_Ammo_Shaped") + " "; + } + guiAmmoTypeString += StringUtils.Localize("#LOC_BDArmory_Ammo_Explosive") + " "; } if (bulletInfo.incendiary) { - guiAmmoTypeString += Localizer.Format("#LOC_BDArmory_Ammo_Incendiary") + " "; + guiAmmoTypeString += StringUtils.Localize("#LOC_BDArmory_Ammo_Incendiary") + " "; } if (bulletInfo.EMP && !bulletInfo.nuclear) { - guiAmmoTypeString += Localizer.Format("#LOC_BDArmory_Ammo_EMP") + " "; + guiAmmoTypeString += StringUtils.Localize("#LOC_BDArmory_Ammo_EMP") + " "; } if (bulletInfo.beehive) { - guiAmmoTypeString += Localizer.Format("#LOC_BDArmory_Ammo_Beehive") + " "; + guiAmmoTypeString += StringUtils.Localize("#LOC_BDArmory_Ammo_Beehive") + " "; } - if (!bulletInfo.explosive && bulletInfo.apBulletMod <= 0.8) + if (bulletInfo.tntMass <= 0 && bulletInfo.apBulletMod <= 0.8) { - guiAmmoTypeString += Localizer.Format("#LOC_BDArmory_Ammo_Slug"); + guiAmmoTypeString += StringUtils.Localize("#LOC_BDArmory_Ammo_Slug"); } } else { - guiAmmoTypeString = Localizer.Format("#LOC_BDArmory_Ammo_Multiple"); + guiAmmoTypeString = StringUtils.Localize("#LOC_BDArmory_Ammo_Multiple"); if (baseBulletVelocity < 0) { baseBulletVelocity = BulletInfo.bullets[customAmmoBelt[0].ToString()].bulletVelocity; } - } } if (eWeaponType == WeaponTypes.Rocket) @@ -4822,54 +5496,60 @@ public void ParseAmmoStats() ProjectileCount = rocketInfo.subProjectileCount; rocketModelPath = rocketInfo.rocketModelPath; SelectedAmmoType = rocketInfo.name; //store selected ammo name as string for retrieval by web orc filter/later GUI implementation - + beehive = rocketInfo.beehive; tntMass = rocketInfo.tntMass; + Impulse = rocketInfo.force; + massAdjustment = rocketInfo.massMod; if (rocketInfo.subProjectileCount > 1) { - guiAmmoTypeString = Localizer.Format("#LOC_BDArmory_Ammo_Shot") + " "; // maybe add an int value to these for future Missilefire SmartPick expansion? For now, choose loadouts carefuly! + guiAmmoTypeString = StringUtils.Localize("#LOC_BDArmory_Ammo_Shot") + " "; // maybe add an int value to these for future Missilefire SmartPick expansion? For now, choose loadouts carefuly! } - if (rocketInfo.explosive) + if (rocketInfo.nuclear) + { + guiAmmoTypeString += StringUtils.Localize("#LOC_BDArmory_Ammo_Nuclear") + " "; + } + if (rocketInfo.explosive && !rocketInfo.nuclear) { if (rocketInfo.flak) { - guiAmmoTypeString += Localizer.Format("#LOC_BDArmory_Ammo_Flak") + " "; + guiAmmoTypeString += StringUtils.Localize("#LOC_BDArmory_Ammo_Flak") + " "; eFuzeType = FuzeTypes.Flak; //fix rockets not getting detonation range slider } else if (rocketInfo.shaped) { - guiAmmoTypeString += Localizer.Format("#LOC_BDArmory_Ammo_Shaped") + " "; + guiAmmoTypeString += StringUtils.Localize("#LOC_BDArmory_Ammo_Shaped") + " "; } if (rocketInfo.EMP || rocketInfo.choker || rocketInfo.impulse) { if (rocketInfo.EMP) { - guiAmmoTypeString += Localizer.Format("#LOC_BDArmory_Ammo_EMP") + " "; + guiAmmoTypeString += StringUtils.Localize("#LOC_BDArmory_Ammo_EMP") + " "; } if (rocketInfo.choker) { - guiAmmoTypeString += Localizer.Format("#LOC_BDArmory_Ammo_Choker") + " "; + guiAmmoTypeString += StringUtils.Localize("#LOC_BDArmory_Ammo_Choker") + " "; } if (rocketInfo.impulse) { - guiAmmoTypeString += Localizer.Format("#LOC_BDArmory_Ammo_Impulse") + " "; + guiAmmoTypeString += StringUtils.Localize("#LOC_BDArmory_Ammo_Impulse") + " "; } } else { - guiAmmoTypeString += Localizer.Format("#LOC_BDArmory_Ammo_HE") + " "; + guiAmmoTypeString += StringUtils.Localize("#LOC_BDArmory_Ammo_HE") + " "; } if (rocketInfo.incendiary) { - guiAmmoTypeString += Localizer.Format("#LOC_BDArmory_Ammo_Incendiary") + " "; + guiAmmoTypeString += StringUtils.Localize("#LOC_BDArmory_Ammo_Incendiary") + " "; } if (rocketInfo.gravitic) { - guiAmmoTypeString += Localizer.Format("#LOC_BDArmory_Ammo_Gravitic") + " "; + guiAmmoTypeString += StringUtils.Localize("#LOC_BDArmory_Ammo_Gravitic") + " "; } } else { - guiAmmoTypeString += Localizer.Format("#LOC_BDArmory_Ammo_Kinetic"); + guiAmmoTypeString += StringUtils.Localize("#LOC_BDArmory_Ammo_Kinetic"); } if (rocketInfo.flak) { @@ -4884,7 +5564,6 @@ public void ParseAmmoStats() electroLaser = rocketInfo.EMP; //borrowing electrolaser bool, should really rename it empWeapon choker = rocketInfo.choker; incendiary = rocketInfo.incendiary; - SelectedAmmoType = rocketInfo.name; //store selected ammo name as string for retrieval by web orc filter/later GUI implementation SetupRocketPool(currentType, rocketModelPath); } PAWRefresh(); @@ -4911,7 +5590,7 @@ protected void SetInitialDetonationDistance() proximityDetonation = false; } } - if (BDArmorySettings.DRAW_DEBUG_LABELS) + if (BDArmorySettings.DEBUG_WEAPONS) { Debug.Log("[BDArmory.ModuleWeapon]: DetonationDistance = : " + detonationRange); } @@ -4994,9 +5673,13 @@ public override string GetInfo() output.AppendLine($"Ammunition: {ammoName}"); if (ECPerShot > 0) { - output.AppendLine($"Electric Charge required per shot: {ammoName}"); + output.AppendLine($"Electric Charge required per shot: {ECPerShot}"); } output.AppendLine($"Max Range: {maxEffectiveDistance} m"); + if (minSafeDistance > 0) + { + output.AppendLine($"Min Range: {minSafeDistance} m"); + } if (weaponType == "ballistic") { for (int i = 0; i < ammoList.Count; i++) @@ -5010,6 +5693,8 @@ public override string GetInfo() continue; } ParseBulletFuzeType(binfo.fuzeType); + ParseBulletHEType(binfo.explosive); + output.AppendLine(""); output.AppendLine($"Bullet type: {(string.IsNullOrEmpty(binfo.DisplayName) ? binfo.name : binfo.DisplayName)}"); output.AppendLine($"Bullet mass: {Math.Round(binfo.bulletMass, 2)} kg"); output.AppendLine($"Muzzle velocity: {Math.Round(binfo.bulletVelocity, 2)} m/s"); @@ -5019,7 +5704,8 @@ public override string GetInfo() output.AppendLine($"Cannister Round"); output.AppendLine($" - Submunition count: {binfo.subProjectileCount}"); } - if (binfo.explosive && !binfo.nuclear) + output.AppendLine($"Estimated Penetration: {ProjectileUtils.CalculatePenetration(binfo.caliber, binfo.bulletVelocity, binfo.bulletMass, binfo.apBulletMod):F2} mm"); + if ((binfo.tntMass > 0) && !binfo.nuclear) { output.AppendLine($"Blast:"); output.AppendLine($"- tnt mass: {Math.Round(binfo.tntMass, 3)} kg"); @@ -5034,6 +5720,9 @@ public override string GetInfo() { output.AppendLine($"Air detonation: False"); } + + if (binfo.explosive.ToLower() == "shaped") + output.AppendLine($"Shaped Charge Penetration: {ProjectileUtils.CalculatePenetration(binfo.caliber > 0 ? binfo.caliber * 0.05f : 6f, 5000f, binfo.tntMass * 0.0555f, binfo.apBulletMod):F2} mm"); } if (binfo.nuclear) { @@ -5069,7 +5758,6 @@ public override string GetInfo() BulletInfo sinfo = BulletInfo.bullets[binfo.subMunitionType.ToString()]; output.AppendLine($"- deploys {sinfo.subProjectileCount}x {(string.IsNullOrEmpty(sinfo.DisplayName) ? sinfo.name : sinfo.DisplayName)}"); } - output.AppendLine(""); } } if (weaponType == "rocket") @@ -5084,16 +5772,27 @@ public override string GetInfo() output.AppendLine(""); continue; } - output.AppendLine($"Rocket type: {ammoList[i]}"); + output.AppendLine($"Rocket type: {(string.IsNullOrEmpty(rinfo.DisplayName) ? rinfo.name : rinfo.DisplayName)}"); output.AppendLine($"Rocket mass: {Math.Round(rinfo.rocketMass * 1000, 2)} kg"); //output.AppendLine($"Thrust: {thrust}kn"); mass and thrust don't really tell us the important bit, so lets replace that with accel output.AppendLine($"Acceleration: {rinfo.thrust / rinfo.rocketMass}m/s2"); - if (rinfo.explosive) + if (rinfo.explosive && !rinfo.nuclear) { output.AppendLine($"Blast:"); output.AppendLine($"- tnt mass: {Math.Round((rinfo.tntMass), 3)} kg"); output.AppendLine($"- radius: {Math.Round(BlastPhysicsUtils.CalculateBlastRange(rinfo.tntMass), 2)} m"); output.AppendLine($"Proximity Fuzed: {rinfo.flak}"); + if (rinfo.shaped) + output.AppendLine($"Estimated Penetration: {ProjectileUtils.CalculatePenetration(rinfo.caliber > 0 ? rinfo.caliber * 0.05f : 6f, 5000f, rinfo.tntMass * 0.0555f, rinfo.apMod):F2} mm"); + } + if (rinfo.nuclear) + { + output.AppendLine($"Nuclear Rocket:"); + output.AppendLine($"- yield: {Math.Round(rinfo.tntMass, 3)} kT"); + if (rinfo.EMP) + { + output.AppendLine($"- generates EMP"); + } } output.AppendLine(""); if (rinfo.subProjectileCount > 1) @@ -5112,9 +5811,9 @@ public override string GetInfo() if (graviticWeapon) { output.AppendLine($"Gravitic warhead:"); - output.AppendLine($"- weight added per part hit:{massAdjustment * 1000} kg"); + output.AppendLine($"- Mass added per part hit:{massAdjustment * 1000} kg"); } - if (electroLaser) + if (electroLaser && !rinfo.nuclear) { output.AppendLine($"EMP warhead:"); output.AppendLine($"- can temporarily shut down targets"); @@ -5129,7 +5828,23 @@ public override string GetInfo() output.AppendLine($"Incendiary:"); output.AppendLine($"- Covers targets in inferno gel"); } + if (rinfo.beehive) + { + output.AppendLine($"Cluster Rocket:"); + if (BulletInfo.bulletNames.Contains(rinfo.subMunitionType)) + { + BulletInfo sinfo = BulletInfo.bullets[rinfo.subMunitionType.ToString()]; + output.AppendLine($"- deploys {sinfo.subProjectileCount}x {(string.IsNullOrEmpty(sinfo.DisplayName) ? sinfo.name : sinfo.DisplayName)}"); + } + else if (RocketInfo.rocketNames.Contains(rinfo.subMunitionType)) + { + RocketInfo sinfo = RocketInfo.rockets[rinfo.subMunitionType.ToString()]; + output.AppendLine($"- deploys {sinfo.subProjectileCount}x {(string.IsNullOrEmpty(sinfo.DisplayName) ? sinfo.name : sinfo.DisplayName)}"); + } + } } + + } if (externalAmmo) { @@ -5154,6 +5869,12 @@ public override string GetInfo() { output.AppendLine($"Crew-served Weapon - Requires onboard Kerbal"); } + if (isAPS) + { + output.AppendLine($"Autonomous Point Defense Weapon"); + output.AppendLine($" - Interception type: {APSType}"); + if (dualModeAPS) output.AppendLine($" - Dual purpose; can be used offensively"); + } return output.ToString(); } @@ -5196,8 +5917,9 @@ public static void HideGUI() { if (instance != null && instance.WPNmodule != null) { - instance.WPNmodule.WeaponName = instance.WPNmodule.shortName; + instance.WPNmodule.WeaponDisplayName = instance.WPNmodule.shortName; instance.WPNmodule = null; + instance.applyWeaponGroupTo = null; instance.UpdateGUIState(); } EditorLogic editor = EditorLogic.fetch; @@ -5212,6 +5934,8 @@ public static void ShowGUI(ModuleWeapon WPNmodule) instance.WPNmodule = WPNmodule; instance.UpdateGUIState(); } + instance.applyWeaponGroupTo = new string[] { "this weapon", "symmetric weapons", $"all {WPNmodule.part.partInfo.title}s", $"all {WPNmodule.GetWeaponClass()}s", "all Guns/Rockets/Lasers" }; + instance._applyWeaponGroupTo = instance.applyWeaponGroupTo[instance._applyWeaponGroupToIndex]; } private void UpdateGUIState() @@ -5319,9 +6043,12 @@ public void OnGUI() { editor.Unlock("BD_MN_GUILock"); } - guiWindowRect = GUILayout.Window(GetInstanceID(), guiWindowRect, GUIWindow, "Weapon Group GUI", Styles.styleEditorPanel); + guiWindowRect = GUILayout.Window(GUIUtility.GetControlID(FocusType.Passive), guiWindowRect, GUIWindow, "Weapon Group GUI", Styles.styleEditorPanel); } + string[] applyWeaponGroupTo; + string _applyWeaponGroupTo; + int _applyWeaponGroupToIndex = 0; public void GUIWindow(int windowID) { InitializeStyles(); @@ -5339,11 +6066,65 @@ public void GUIWindow(int windowID) { string newName = string.IsNullOrEmpty(txtName.Trim()) ? WPNmodule.OriginalShortName : txtName.Trim(); - WPNmodule.WeaponName = newName; - WPNmodule.shortName = newName; + switch (_applyWeaponGroupToIndex) + { + case 0: + WPNmodule.WeaponDisplayName = newName; + WPNmodule.shortName = newName; + break; + case 1: // symmetric parts + WPNmodule.WeaponDisplayName = newName; + WPNmodule.shortName = newName; + foreach (Part p in WPNmodule.part.symmetryCounterparts) + { + var wpn = p.GetComponent(); + if (wpn == null) continue; + wpn.WeaponDisplayName = newName; + wpn.shortName = newName; + } + break; + case 2: // all weapons of the same type + foreach (Part p in EditorLogic.fetch.ship.parts) + { + if (p.name == WPNmodule.part.name) + { + var wpn = p.GetComponent(); + if (wpn == null) continue; + wpn.WeaponDisplayName = newName; + wpn.shortName = newName; + } + } + break; + case 3: // all weapons of the same class + var wpnClass = WPNmodule.GetWeaponClass(); + foreach (Part p in EditorLogic.fetch.ship.parts) + { + var wpn = p.GetComponent(); + if (wpn == null) continue; + if (wpn.GetWeaponClass() != wpnClass) continue; + wpn.WeaponDisplayName = newName; + wpn.shortName = newName; + } + break; + case 4: // all guns/rockets/lasers + var gunsRocketsLasers = new HashSet { WeaponClasses.Gun, WeaponClasses.Rocket, WeaponClasses.DefenseLaser }; + foreach (Part p in EditorLogic.fetch.ship.parts) + { + var wpn = p.GetComponent(); + if (wpn == null) continue; + if (!gunsRocketsLasers.Contains(wpn.GetWeaponClass())) continue; + wpn.WeaponDisplayName = newName; + wpn.shortName = newName; + } + break; + } instance.WPNmodule.HideUI(); } + GUILayout.EndHorizontal(); + GUILayout.BeginHorizontal(); + GUILayout.Label($"Apply to {_applyWeaponGroupTo}"); + if (_applyWeaponGroupToIndex != (_applyWeaponGroupToIndex = Mathf.RoundToInt(GUILayout.HorizontalSlider(_applyWeaponGroupToIndex, 0, 4, GUILayout.Width(150))))) _applyWeaponGroupTo = applyWeaponGroupTo[_applyWeaponGroupToIndex]; GUILayout.EndHorizontal(); scrollPos = GUILayout.BeginScrollView(scrollPos); @@ -5353,7 +6134,7 @@ public void GUIWindow(int windowID) GUILayout.EndVertical(); GUI.DragWindow(); - BDGUIUtils.RepositionWindow(ref guiWindowRect); + GUIUtils.RepositionWindow(ref guiWindowRect); } private static void InitializeStyles() diff --git a/BDArmory/Misc/RippleOption.cs b/BDArmory/Weapons/RippleOption.cs similarity index 89% rename from BDArmory/Misc/RippleOption.cs rename to BDArmory/Weapons/RippleOption.cs index a33a3466c..f127ca4f3 100644 --- a/BDArmory/Misc/RippleOption.cs +++ b/BDArmory/Weapons/RippleOption.cs @@ -1,4 +1,4 @@ -namespace BDArmory.Misc +namespace BDArmory.Weapons { public class RippleOption { diff --git a/BDArmory/.ksplocalizer.settings b/BDArmory/_unused_old_code/.ksplocalizer.settings similarity index 100% rename from BDArmory/.ksplocalizer.settings rename to BDArmory/_unused_old_code/.ksplocalizer.settings diff --git a/BDArmory/Distribution/GameData/BDArmory/KSPedia/bdac addons.ksp b/BDArmory/_unused_old_code/KSPedia/bdac addons.ksp similarity index 100% rename from BDArmory/Distribution/GameData/BDArmory/KSPedia/bdac addons.ksp rename to BDArmory/_unused_old_code/KSPedia/bdac addons.ksp diff --git a/BDArmory/Distribution/GameData/BDArmory/KSPedia/bdac ammunition.ksp b/BDArmory/_unused_old_code/KSPedia/bdac ammunition.ksp similarity index 100% rename from BDArmory/Distribution/GameData/BDArmory/KSPedia/bdac ammunition.ksp rename to BDArmory/_unused_old_code/KSPedia/bdac ammunition.ksp diff --git a/BDArmory/Distribution/GameData/BDArmory/KSPedia/bdac control systems.ksp b/BDArmory/_unused_old_code/KSPedia/bdac control systems.ksp similarity index 100% rename from BDArmory/Distribution/GameData/BDArmory/KSPedia/bdac control systems.ksp rename to BDArmory/_unused_old_code/KSPedia/bdac control systems.ksp diff --git a/BDArmory/Distribution/GameData/BDArmory/KSPedia/bdac countermeasures.ksp b/BDArmory/_unused_old_code/KSPedia/bdac countermeasures.ksp similarity index 100% rename from BDArmory/Distribution/GameData/BDArmory/KSPedia/bdac countermeasures.ksp rename to BDArmory/_unused_old_code/KSPedia/bdac countermeasures.ksp diff --git a/BDArmory/Distribution/GameData/BDArmory/KSPedia/bdac faq.ksp b/BDArmory/_unused_old_code/KSPedia/bdac faq.ksp similarity index 100% rename from BDArmory/Distribution/GameData/BDArmory/KSPedia/bdac faq.ksp rename to BDArmory/_unused_old_code/KSPedia/bdac faq.ksp diff --git a/BDArmory/Distribution/GameData/BDArmory/KSPedia/bdac fixed guns.ksp b/BDArmory/_unused_old_code/KSPedia/bdac fixed guns.ksp similarity index 100% rename from BDArmory/Distribution/GameData/BDArmory/KSPedia/bdac fixed guns.ksp rename to BDArmory/_unused_old_code/KSPedia/bdac fixed guns.ksp diff --git a/BDArmory/Distribution/GameData/BDArmory/KSPedia/bdac guidance types.ksp b/BDArmory/_unused_old_code/KSPedia/bdac guidance types.ksp similarity index 100% rename from BDArmory/Distribution/GameData/BDArmory/KSPedia/bdac guidance types.ksp rename to BDArmory/_unused_old_code/KSPedia/bdac guidance types.ksp diff --git a/BDArmory/Distribution/GameData/BDArmory/KSPedia/bdac modules.ksp b/BDArmory/_unused_old_code/KSPedia/bdac modules.ksp similarity index 100% rename from BDArmory/Distribution/GameData/BDArmory/KSPedia/bdac modules.ksp rename to BDArmory/_unused_old_code/KSPedia/bdac modules.ksp diff --git a/BDArmory/Distribution/GameData/BDArmory/KSPedia/bdac ordinance.ksp b/BDArmory/_unused_old_code/KSPedia/bdac ordinance.ksp similarity index 100% rename from BDArmory/Distribution/GameData/BDArmory/KSPedia/bdac ordinance.ksp rename to BDArmory/_unused_old_code/KSPedia/bdac ordinance.ksp diff --git a/BDArmory/Distribution/GameData/BDArmory/KSPedia/bdac radar and targeting.ksp b/BDArmory/_unused_old_code/KSPedia/bdac radar and targeting.ksp similarity index 100% rename from BDArmory/Distribution/GameData/BDArmory/KSPedia/bdac radar and targeting.ksp rename to BDArmory/_unused_old_code/KSPedia/bdac radar and targeting.ksp diff --git a/BDArmory/Distribution/GameData/BDArmory/KSPedia/bdac settings.ksp b/BDArmory/_unused_old_code/KSPedia/bdac settings.ksp similarity index 100% rename from BDArmory/Distribution/GameData/BDArmory/KSPedia/bdac settings.ksp rename to BDArmory/_unused_old_code/KSPedia/bdac settings.ksp diff --git a/BDArmory/Distribution/GameData/BDArmory/KSPedia/bdac turrets.ksp b/BDArmory/_unused_old_code/KSPedia/bdac turrets.ksp similarity index 100% rename from BDArmory/Distribution/GameData/BDArmory/KSPedia/bdac turrets.ksp rename to BDArmory/_unused_old_code/KSPedia/bdac turrets.ksp diff --git a/BDArmory/Modules/Animation/BDALookConstraintUp.cs b/BDArmory/_unused_old_code/Modules/Animation/BDALookConstraintUp.cs similarity index 100% rename from BDArmory/Modules/Animation/BDALookConstraintUp.cs rename to BDArmory/_unused_old_code/Modules/Animation/BDALookConstraintUp.cs diff --git a/BDArmory/Modules/Animation/BDAScaleByDistance.cs b/BDArmory/_unused_old_code/Modules/Animation/BDAScaleByDistance.cs similarity index 100% rename from BDArmory/Modules/Animation/BDAScaleByDistance.cs rename to BDArmory/_unused_old_code/Modules/Animation/BDAScaleByDistance.cs diff --git a/BDArmory/Modules/ModuleWWC.cs b/BDArmory/_unused_old_code/Modules/ModuleWWC.cs similarity index 98% rename from BDArmory/Modules/ModuleWWC.cs rename to BDArmory/_unused_old_code/Modules/ModuleWWC.cs index a1eb46a4b..002e815ad 100644 --- a/BDArmory/Modules/ModuleWWC.cs +++ b/BDArmory/_unused_old_code/Modules/ModuleWWC.cs @@ -1,5 +1,7 @@ using System.Collections.Generic; +using BDArmory.Weapons; + namespace BDArmory.Modules { public class ModuleWWC : PartModule diff --git a/BDArmory.Core/PerformanceLogger.cs b/BDArmory/_unused_old_code/PerformanceLogger.cs similarity index 99% rename from BDArmory.Core/PerformanceLogger.cs rename to BDArmory/_unused_old_code/PerformanceLogger.cs index 9f9842242..e1a9eab55 100644 --- a/BDArmory.Core/PerformanceLogger.cs +++ b/BDArmory/_unused_old_code/PerformanceLogger.cs @@ -7,7 +7,7 @@ using UnityEngine; using Debug = UnityEngine.Debug; -namespace BDArmory.Core +namespace BDArmory { [KSPAddon(KSPAddon.Startup.Flight, false)] public class PerformanceLogger : MonoBehaviour, IDisposable diff --git a/BDArmory/SmartFindTarget.dgml b/BDArmory/_unused_old_code/SmartFindTarget.dgml similarity index 100% rename from BDArmory/SmartFindTarget.dgml rename to BDArmory/_unused_old_code/SmartFindTarget.dgml diff --git a/Notes.md b/Notes.md new file mode 100644 index 000000000..628185e06 --- /dev/null +++ b/Notes.md @@ -0,0 +1,43 @@ +### Branches +Current un-merged branches (`git branch --no-merged`) are: +- AoA — respecting maxAoA and max G-load AI settings +- bias-testing — testing bias due to spawn position or camera focus +- motherships — which AI/WM is in control when parasite fighters get detached and automatically adding them to competitions + +Outdated, probably to be deleted: +- spawn-strategy — partially implemented, then abandoned spawn strategy implementation by aubranium +- sph-inertia — Simple flight dynamics analysis draft impl by aubranium, better as a separate mod since it doesn't use anything BDA specific. + + +### Optimisation +- https://learn.unity.com/tutorial/fixing-performance-problems-2019-3-1# +- Various setters/accessors in Unity perform extra operations that may cause GC allocations or have other overheads: + - Setting a transform's position/rotation causes OnTransformChanged events for all child transforms. + - Prefer Transform.localPosition over Transform.position when possible or cache Transform.position as Transform.position calculates world position each time it's accessed. + - Check if a field is actually a getter and cache the result instead of repeated get calls. +- Strings cause a lot of GC alloc. + - Use interpolated strings or StringBuilder instead of concatenating strings. + - UnityEngine.Object.name allocates a new string (Object.get_name). + - Localizer.Format strings should be cached as they don't change during the game — StringUtils.cs + - AddVesselSwitcherWindowEntry and WindowVesselSwitcher in LoadedVesselSwitcher.cs and WindowVesselSpawner in VesselSpawnerWindow.cs are doing a lot of string manipulation. + - KerbalEngineer does a lot of string manipulation. + - vessel.vesselName and vessel.GetName() are fine. vessel.GetDisplayName() is bad! +- Tuples are classes (allocated on the heap), ValueTuples are structs (allocated on the stack). Use ValueTuples to avoid GC allocations. +- Use non-allocating versions of RaycastAll, OverlapSphere and similar (Raycast uses the stack so it's fine). +- The break-even point for using RaycastCommand instead of multiple Raycasts seems to be around 8 raycasts. Also, until Unity 2022.2, RaycastCommand only returns the first hit per job. +- Cache "Wait..." yield instructions instead of using "new Wait...". +- Starting coroutines causes some GC — avoid starting them in Update or FixedUpdate. +- Avoid Linq expressions in critical areas. However, some Linq queries can be parallelised (PLINQ) with ".AsParallel()" and sequentialised with ".AsSequential()". Also, ".ForEach()" does a merge to sequential, while ".ForAll()" doesn't. +- Avoid excessive object references in structs and classes and prefer identifiers instead — affects GC checks. +- Trigger GC manually at appropriate times (System.GC.Collect()) when it won't affect gameplay, e.g., when resetting competition stuff. + +- Bad GC routines: + - part.explode when triggering new vessels causes massive GC alloc, but it's in base KSP, so there's not much that can be done. + - ExplosionFX.IsInLineOfSight — Sorting of the raycast hits by distance causes GC alloc, but using Array.Copy and Array.Sort is the best I've managed to find, certainly much better than Linq and Lists. + - MissileFire.GuardTurretRoutine -> RadarUtils.RenderVesselRadarSnapshot -> GetPixels32 — Not much we can do about this. Also, GetPixels actually leaks memory! + - PartResourceList: Part.Resources.GetEnumerator causes GC alloc. Using Part.Resources.dict.Values.GetEnumerator seems better? + - VesselSpawnerWindow.WindowVesselSpawner -> string manipulation + - LoadedVesselSwitcher.WindowVesselSwitcher -> string manipulation + - LoadedVesselSwitcher.AddVesselSwitcherWindowEntry -> string manipulation + - CamTools.SetDoppler -> get_name + - CameraTools::CTPartAudioController.Awake diff --git a/README.md b/README.md index 392ee61f6..7610fc5c1 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,33 @@ -BDArmory -======== +BDArmory Plus +============= Gun turrets and other weapon systems for KSP Original Author [BahamutoD](https://github.com/BahamutoD) -Original [Forum link](http://forum.kerbalspaceprogram.com/threads/85209-BDArmory) +Current [Forum link](https://forum.kerbalspaceprogram.com/index.php?/topic/209092-19x-112x-bdarmory-plus-bda-2022-07-23/) + +BDAc [Forum link](https://forum.kerbalspaceprogram.com/index.php?/topic/184167-17x-bdarmory-continued-v130-05012019/) -Current [Forum link](https://forum.kerbalspaceprogram.com/index.php?/topic/184167-17x-bdarmory-continued-v130-05012019/) +Original [Forum link](http://forum.kerbalspaceprogram.com/threads/85209-BDArmory) Current Maintainers: - [DocNappers](https://github.com/BrettRyland) - [Josue](https://github.com/josuenos) - [SuicidalInsanity](https://github.com/SuicidalInsanity) -- [Aubranium](https://github.com/agoodman) (Remote Orchestration) +- [Bill Nye](https://github.com/BillNyeTheIE) Contributors: +- [Aubranium](https://github.com/agoodman) (Remote Orchestration) - [Scott Manley](https://github.com/illectro) - [Stardust](https://github.com/Stardust-Rapture) - [Kurgan](https://github.com/TheKurgan) +- [Kaz]() - [CeruleanEyes]() - [Fluffy]() - [Cl0by](https://github.com/Cl0by) +- [Concodroid]() +- [EzBro]() - And all the previous maintainers. Previous Maintainers: @@ -53,4 +59,4 @@ This mod for Kerbal Space Program was originally developed by Paolo Encarnacion This mod is now being maintained in BahamutoD's absence by Joe Korinek (Papa_Joe) and continues to be distributed under the license CC-BY-SA 2.0. Please read about the license at https://creativecommons.org/licenses/by-sa/2.0/ -before attempting to modify and redistribute it. \ No newline at end of file +before attempting to modify and redistribute it. diff --git a/TODO.md b/TODO.md new file mode 100644 index 000000000..f748b43fe --- /dev/null +++ b/TODO.md @@ -0,0 +1,85 @@ +### Bugs +- Auto-tuning with numeric input fields enabled in the AI GUI won't let the values change +- Changing the slider resolution sometimes triggers clamping of unclamped values +- HP of asteroids in the SPH is wrong. + + +### TODO (smaller items and specific requests / higher priority) +- Fix bugs + +- Wiki entries + - Auto-Tuning + +- ? Add an action group trigger to the WM based on the current target being an enemy vessel within a custom distance. - Make it a collapsable section of custom triggers to include other conditions later. +- Artillery aiming support +- Lift stacking improvements with logical wing segments +- Add a distance based modifier to PID: lower P, higher D at longer distances. +- Add a Panic Button to the AI that triggers an action group, triggered by: + - Being in a flat spin for X seconds while below min altitude and less than Y seconds from impact. + - Being stalled for X seconds while below min altitude and less than Y seconds from impact. — define "stalled" + +- Gauntlet mode doesn't properly randomise teams/spawning? - check +- Fix the piñata spawning logic - spawn the piñata(s) separately after circular spawning has occured. +- Motherships branch +- Inertial correction to pitch, roll, yaw errors for PID calcuations. Rotate the vessel reference transform first, computing debugPos2 from the top +- Preallocate the explosion merge buffer and reuse it. +- If re-assign teams is disabled, custom spawn templates should use the team name of the first vessel in each team as the team name. +- Fix the AIGUI slider limits and increments for the variable precision slider in the PAW +- Low altitude AI setting should be aware of killer GM low altitude. +- Adjust explosions to maintain velocity or not based on atmospheric density. +- Add VTOL AI to the AI GUI. +- Proper 3-axis PID sliders + single PID with axis weighting +- Memory for AI state so that it can resume once finished extending/evading instead of just scanning for new targets. +- KerbalSafety - why are some kerbals not registering/deploying chutes +- Tag mode should disable team icons to get colours right +- Improve the VTOL AI: + - Terrain avoidance + - Other logic from the pilot AI. + +- BDAVesselMover + - Camera behaviour is weird if the mouse is over various windows, also when CameraTools is enabled above 100km — both are likely related to krakensbane + - PRE can sometimes break KSP — not sure there's much we can do? maybe check if something steals the camera and switch back again? + + +### Ideas (more general things / lower priority) +- Meteor Cannon: Summons an asteroid just in front of the vessel, then accelerates it to ~1000m/s (10x force pushing asteroid, 1x force pushing back on vessel) + - Meteor Cannon class has its own selection of meteors that it randomly selects from when instantiating it. Generate at start and replenish as necessary. +- Add a PID_NeuralCoprocessor — A small FC neural network with configurable depth to modify the PID by ±pid (separate scale per channel) that learns when enabled +- AutoPilot: + - On takeoff, look diagonally down and turn if the terrain normal is too steep. +- Autotuning: + - Make the fly-to points dynamic instead of static (e.g., move sideways at fixed velocity) to avoid under-tuning I. +- Evasion/Strafing + - When attacking a ground target with a turreted gun with at least 90° yaw, aim to circle around at ~2*turn radius at default altitude instead of strafing it directly. Adjust for min/max gun range. Don't use strafing speed (use cruise speed?). +- Waypoints + - Use a spline between current position and velocity, waypoint and next waypoint +- Add BDA's FlyToPosition as the first flight controller in MouseAimFlight via reflection. +- Tournaments + - Add in-built score and ranked tournaments. + - Add pop-up for entering score weights in the vessel switcher and sort by score instead of just hits. Right click sorting button should toggle this pop-up + - This would allow for ranked tournaments and other tournament styles + - Add a randomised vs ranked toggle for heat generation. + - ranked => dynamic round allocation instead of pre-determined. + - In ranked mode the deficit should be distributed amongst the top ranked heats. + - Boss fight tournament mode +- Record starting conditions for bullets, position every 1000m and time and position of first impact in vessel traces. Also, bullet type. This should be sufficient for approximate curves in blender and colours, etc. can be found from the configs. +- Add a max morgue capacity and recycle kerbals once it's full. Regen the main 4 and discard the others? Would need special handling for custom kerbals. +- Profile the infinite ordinance option for spawning missile parts. "I wonder if it's possible to avoid a lot of the spawning cost and memory leakage by detaching the Vessel component from the missile prior to getting destroyed, packing and disabling it, then attaching, unpacking and enabling it on a new missile? Something to look at in the future..." +- Strafing planes are wobbly initially (maybe at low speeds in general?) + + +### Older notes (may not be still valid) +- Check the physx branch for changes related to using accelerated time. +- Time scaling unpauses the game and is undone by manually pausing/unpausing. +- Allow parsing multiple tournaments as a single large tournament +- Add a auto-link function/option (UI toggle + action group options) to the radar data receiver. +- Remove dead kerbals from the roster +- Completely disable ramming logic and scores when "disable ramming" is globally enabled +- using the clamp/unclamp option from the AI tab UI resets unclamped values to clamped maximum when re-clamping (check: is this still an issue?) +- Add a tournament option for having one "boss" team that each other team fights each round +- Do the same vessel-relative checks for rockets +- Once the final target has been acquired in aiming, do a simulation with raycasts to check for obstacles in the way. If the target is blocked, then give it a modifier for target selection in the future that slowly recovers. +- Kerbal Safety: if a NaN orbit is detected, try setting the orbit of the kerbal based on the orbit of the part it left. + + +- See https://github.com/BrettRyland/BDArmory/issues/50 \ No newline at end of file diff --git a/DamageCurves/ArmorCurve.txt b/_Other Stuff/DamageCurves/ArmorCurve.txt similarity index 100% rename from DamageCurves/ArmorCurve.txt rename to _Other Stuff/DamageCurves/ArmorCurve.txt diff --git a/_Other Stuff/get_craft.py b/_Other Stuff/get_craft.py new file mode 100644 index 000000000..e19bc10a2 --- /dev/null +++ b/_Other Stuff/get_craft.py @@ -0,0 +1,70 @@ +# Standard library imports +import argparse +import json +import os +import re +import subprocess +import tempfile +from pathlib import Path +from shutil import which + +parser = argparse.ArgumentParser(description="Grab all the craft from the API for a competition and rename them.", formatter_class=argparse.ArgumentDefaultsHelpFormatter) +parser.add_argument('compID', type=int, help="Competition ID") +parser.add_argument('--curl-command', type=str, default='curl', help="Curl command (in case it needs specifying on Windows).") +parser.add_argument('--keep', action='store_true', help="Keep the player and manifest files.") +args = parser.parse_args() + +if which(args.curl_command) is None: + print(f"Error: curl command ({args.curl_command}) not found. Please provide a valid curl command as an argument.") + exit() + +comp_url = f"https://conquertheair.com/competitions/{args.compID}/vessels/manifest.json" +players_url = f"https://conquertheair.com/players" +cwd = Path('.').absolute() +manifest_file = Path(tempfile.mkstemp(dir=cwd if args.keep else None)[1]) +players_file = Path(tempfile.mkstemp(dir=cwd if args.keep else None)[1]) + +try: + print("Fetching players and competition manifest...", end='', flush=True) + subprocess.run(f"{args.curl_command} -s {players_url} -o {players_file}".split()) + subprocess.run(f"{args.curl_command} -s {comp_url} -o {manifest_file}".split()) + print("done.") + + with open(players_file, 'r') as f: + lines = f.readlines() + expr = re.compile('players/([0-9]+)">(.*)') + playerIDs = {} + for line in lines: + m = expr.search(line) + if m: + playerIDs[int(m.groups()[0])] = m.groups()[1] + + with open(manifest_file, 'r') as f: + manifest = json.load(f) + if "status" in manifest and manifest["status"] == 404: + print(f"No competition with ID {args.compID}") + else: + print(f"Fetching {len(manifest)} craft files...", end='', flush=True) + for craft in manifest: + craft_name = f"{playerIDs[craft['player_id']]}_{craft['name']}".replace(os.path.sep, '_') + subprocess.run([args.curl_command, "-s", f"{craft['craft_url']}", "-o", f"{craft_name}.craft"]) + with open(f"{craft_name}.craft", 'r+') as f: + lines = f.read().splitlines() + lines[0] = f"ship = {craft_name}" + f.seek(0, 0) + f.truncate() + f.write('\n'.join(lines)) + print('.', end='', flush=True) + print("done.") + +finally: + if not args.keep: + if manifest_file.exists(): + manifest_file.unlink() + if players_file.exists(): + players_file.unlink() + else: + if manifest_file.exists(): + manifest_file.rename('manifest.json') + if players_file.exists(): + players_file.rename('players.html')